Implement noadjustfee, feebumpindex and v parameters for payjoin

This commit is contained in:
nicolas.dorier
2020-05-09 19:59:21 +09:00
parent da588380ed
commit b0f820e95a
4 changed files with 51 additions and 18 deletions

View File

@@ -28,6 +28,7 @@ using NBitcoin.Altcoins;
using NBitcoin.Payment; using NBitcoin.Payment;
using NBitpayClient; using NBitpayClient;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium; using OpenQA.Selenium;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@@ -587,6 +588,14 @@ namespace BTCPayServer.Tests
//Let's start the harassment //Let's start the harassment
invoice = receiverUser.BitPay.CreateInvoice( invoice = receiverUser.BitPay.CreateInvoice(
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true}); new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
// Bad version should throw incorrect version
var endpoint = TestAccount.GetPayjoinEndpoint(invoice, btcPayNetwork.NBitcoinNetwork);
var response = await tester.PayTester.HttpClient.PostAsync(endpoint.AbsoluteUri + "?v=2",
new StringContent("", Encoding.UTF8, "text/plain"));
Assert.False(response.IsSuccessStatusCode);
var error = JObject.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal("version-unsupported", error["errorCode"].Value<string>());
Assert.Equal(1, ((JArray)error["supported"]).Single().Value<int>());
var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork); tester.ExplorerClient.Network.NBitcoinNetwork);
@@ -665,6 +674,7 @@ namespace BTCPayServer.Tests
//Attempt 2: Create two transactions using different inputs and send them to the same invoice. //Attempt 2: Create two transactions using different inputs and send them to the same invoice.
//Result: Second Tx should be rejected. //Result: Second Tx should be rejected.
var Invoice1Coin1ResponseTx = await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork); var Invoice1Coin1ResponseTx = await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork);
await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork, "already-paid"); await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork, "already-paid");
var contributedInputsInvoice1Coin1ResponseTx = var contributedInputsInvoice1Coin1ResponseTx =
Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut); Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut);

View File

@@ -393,7 +393,7 @@ namespace BTCPayServer.Tests
return response; return response;
} }
private static Uri GetPayjoinEndpoint(Invoice invoice, Network network) public static Uri GetPayjoinEndpoint(Invoice invoice, Network network)
{ {
var parsedBip21 = new BitcoinUrlBuilder( var parsedBip21 = new BitcoinUrlBuilder(
invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21, invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21,

View File

@@ -23,6 +23,7 @@ using NBXplorer.DerivationStrategy;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using BTCPayServer.Data; using BTCPayServer.Data;
using NBitcoin.DataEncoders; using NBitcoin.DataEncoders;
using Amazon.S3.Model;
namespace BTCPayServer.Payments.PayJoin namespace BTCPayServer.Payments.PayJoin
{ {
@@ -115,8 +116,20 @@ namespace BTCPayServer.Payments.PayJoin
[EnableCors(CorsPolicies.All)] [EnableCors(CorsPolicies.All)]
[MediaTypeConstraint("text/plain")] [MediaTypeConstraint("text/plain")]
[RateLimitsFilter(ZoneLimits.PayJoin, Scope = RateLimitsScope.RemoteAddress)] [RateLimitsFilter(ZoneLimits.PayJoin, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> Submit(string cryptoCode) public async Task<IActionResult> Submit(string cryptoCode,
bool noadjustfee = false,
int feebumpindex = -1,
int v = 1)
{ {
if (v != 1)
{
return BadRequest(new JObject
{
new JProperty("errorCode", "version-unsupported"),
new JProperty("supported", new JArray(1)),
new JProperty("message", "This version of payjoin is not supported.")
});
}
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode); var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network == null) if (network == null)
{ {
@@ -146,26 +159,28 @@ namespace BTCPayServer.Payments.PayJoin
Transaction originalTx = null; Transaction originalTx = null;
FeeRate originalFeeRate = null; FeeRate originalFeeRate = null;
bool psbtFormat = true; bool psbtFormat = true;
if (!PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt))
if (PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt))
{
if (!psbt.IsAllFinalized())
return BadRequest(CreatePayjoinError("psbt-not-finalized", "The PSBT should be finalized"));
originalTx = psbt.ExtractTransaction();
}
// BTCPay Server implementation support a transaction instead of PSBT
else
{ {
psbtFormat = false; psbtFormat = false;
if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx))
return BadRequest(CreatePayjoinError("invalid-format", "invalid transaction or psbt")); return BadRequest(CreatePayjoinError("invalid-format", "invalid transaction or psbt"));
originalTx = tx; originalTx = tx;
psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork);
psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() {PSBT = psbt})).PSBT; psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT;
for (int i = 0; i < tx.Inputs.Count; i++) for (int i = 0; i < tx.Inputs.Count; i++)
{ {
psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig; psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig;
psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript; psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript;
} }
} }
else
{
if (!psbt.IsAllFinalized())
return BadRequest(CreatePayjoinError("psbt-not-finalized", "The PSBT should be finalized"));
originalTx = psbt.ExtractTransaction();
}
async Task BroadcastNow() async Task BroadcastNow()
{ {
@@ -173,8 +188,6 @@ namespace BTCPayServer.Payments.PayJoin
} }
var sendersInputType = psbt.GetInputsScriptPubKeyType(); var sendersInputType = psbt.GetInputsScriptPubKeyType();
if (sendersInputType is null)
return BadRequest(CreatePayjoinError("unsupported-inputs", "Payjoin only support segwit inputs (of the same type)"));
if (psbt.CheckSanity() is var errors && errors.Count != 0) if (psbt.CheckSanity() is var errors && errors.Count != 0)
{ {
return BadRequest(CreatePayjoinError("insane-psbt", $"This PSBT is insane ({errors[0]})")); return BadRequest(CreatePayjoinError("insane-psbt", $"This PSBT is insane ({errors[0]})"));
@@ -242,7 +255,7 @@ namespace BTCPayServer.Payments.PayJoin
//this should never happen, unless the store owner changed the wallet mid way through an invoice //this should never happen, unless the store owner changed the wallet mid way through an invoice
return StatusCode(500, CreatePayjoinError("unavailable", $"This service is unavailable for now")); return StatusCode(500, CreatePayjoinError("unavailable", $"This service is unavailable for now"));
} }
if (sendersInputType != receiverInputsType) if (sendersInputType is ScriptPubKeyType t && t != receiverInputsType)
{ {
return StatusCode(503, return StatusCode(503,
CreatePayjoinError("out-of-utxos", CreatePayjoinError("out-of-utxos",
@@ -331,6 +344,10 @@ namespace BTCPayServer.Payments.PayJoin
var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index]; var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index];
HashSet<TxOut> isOurOutput = new HashSet<TxOut>(); HashSet<TxOut> isOurOutput = new HashSet<TxOut>();
isOurOutput.Add(ourNewOutput); isOurOutput.Add(ourNewOutput);
TxOut preferredFeeBumpOutput = feebumpindex >= 0
&& feebumpindex < newTx.Outputs.Count
&& !isOurOutput.Contains(newTx.Outputs[feebumpindex])
? newTx.Outputs[feebumpindex] : null;
var rand = new Random(); var rand = new Random();
int senderInputCount = newTx.Inputs.Count; int senderInputCount = newTx.Inputs.Count;
foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value)) foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value))
@@ -398,7 +415,7 @@ namespace BTCPayServer.Payments.PayJoin
Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate); Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate);
Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
Money additionalFee = expectedFee - actualFee; Money additionalFee = expectedFee - actualFee;
if (additionalFee > Money.Zero) if (additionalFee > Money.Zero && !noadjustfee)
{ {
// If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy) // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy)
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++) for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++)
@@ -418,6 +435,9 @@ namespace BTCPayServer.Payments.PayJoin
// The rest, we take from user's change // The rest, we take from user's change
for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero; i++) for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero; i++)
{ {
if (preferredFeeBumpOutput is TxOut &&
preferredFeeBumpOutput != newTx.Outputs[i])
continue;
if (!isOurOutput.Contains(newTx.Outputs[i])) if (!isOurOutput.Contains(newTx.Outputs[i]))
{ {
var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value); var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value);
@@ -493,6 +513,7 @@ namespace BTCPayServer.Payments.PayJoin
.ToDictionary(pair => pair.Key, pair => pair.Value) .ToDictionary(pair => pair.Key, pair => pair.Value)
}); });
// BTCPay Server support PSBT set as hex
if (psbtFormat && HexEncoder.IsWellFormed(rawBody)) if (psbtFormat && HexEncoder.IsWellFormed(rawBody))
{ {
return Ok(newPsbt.ToHex()); return Ok(newPsbt.ToHex());
@@ -501,6 +522,7 @@ namespace BTCPayServer.Payments.PayJoin
{ {
return Ok(newPsbt.ToBase64()); return Ok(newPsbt.ToBase64());
} }
// BTCPay Server should returns transaction if received transaction
else else
return Ok(newTx.ToHex()); return Ok(newTx.ToHex());
} }

View File

@@ -6,9 +6,7 @@ using System.Net.Http;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Payments.Changelly.Models;
using Google.Apis.Http; using Google.Apis.Http;
using Microsoft.WindowsAzure.Storage.Queue.Protocol;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -265,7 +263,8 @@ namespace BTCPayServer.Services
OutOfUTXOS, OutOfUTXOS,
NotEnoughMoney, NotEnoughMoney,
InsanePSBT, InsanePSBT,
VersionUnsupported VersionUnsupported,
NeedUTXOInformation
} }
public class PayjoinReceiverException : PayjoinException public class PayjoinReceiverException : PayjoinException
{ {
@@ -282,6 +281,7 @@ namespace BTCPayServer.Services
"not-enough-money" => PayjoinReceiverWellknownErrors.NotEnoughMoney, "not-enough-money" => PayjoinReceiverWellknownErrors.NotEnoughMoney,
"insane-psbt" => PayjoinReceiverWellknownErrors.InsanePSBT, "insane-psbt" => PayjoinReceiverWellknownErrors.InsanePSBT,
"version-unsupported" => PayjoinReceiverWellknownErrors.VersionUnsupported, "version-unsupported" => PayjoinReceiverWellknownErrors.VersionUnsupported,
"need-utxo-information" => PayjoinReceiverWellknownErrors.NeedUTXOInformation,
_ => null _ => null
}; };
} }
@@ -310,6 +310,7 @@ namespace BTCPayServer.Services
"not-enough-money" => "The receiver added some inputs but could not bump the fee of the payjoin proposal.", "not-enough-money" => "The receiver added some inputs but could not bump the fee of the payjoin proposal.",
"insane-psbt" => "Some consistency check on the PSBT failed.", "insane-psbt" => "Some consistency check on the PSBT failed.",
"version-unsupported" => "This version of payjoin is not supported.", "version-unsupported" => "This version of payjoin is not supported.",
"need-utxo-information" => "The witness UTXO or non witness UTXO is missing",
_ => "Unknown error" _ => "Unknown error"
}; };
} }