mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 22:46:49 +01:00
Simplify RBF handling, and handle case of double spend happening outside of wallet (Fix #1375)
This commit is contained in:
parent
67befcc629
commit
42152050a3
2 changed files with 87 additions and 86 deletions
|
@ -1101,6 +1101,45 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal(payment2, invoice.BtcPaid);
|
Assert.Equal(payment2, invoice.BtcPaid);
|
||||||
Assert.Equal("False", invoice.ExceptionStatus.ToString());
|
Assert.Equal("False", invoice.ExceptionStatus.ToString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Logs.Tester.LogInformation($"Let's test out rbf payments where the payment gets sent elsehwere instead");
|
||||||
|
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||||
|
{
|
||||||
|
Price = 0.01m,
|
||||||
|
Currency = "BTC"
|
||||||
|
}, Facade.Merchant);
|
||||||
|
|
||||||
|
var invoice2Address = BitcoinAddress.Create(invoice2.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
|
||||||
|
uint256 invoice2tx1Id = await tester.ExplorerNode.SendToAddressAsync(invoice2Address, invoice2.BtcDue, replaceable: true);
|
||||||
|
Transaction invoice2Tx1 = null;
|
||||||
|
TestUtils.Eventually(() =>
|
||||||
|
{
|
||||||
|
invoice2 = user.BitPay.GetInvoice(invoice2.Id);
|
||||||
|
Assert.Equal("paid", invoice2.Status);
|
||||||
|
invoice2Tx1 = tester.ExplorerNode.GetRawTransaction(new uint256(invoice2tx1Id));
|
||||||
|
});
|
||||||
|
var invoice2Tx2 = invoice2Tx1.Clone();
|
||||||
|
foreach (var input in invoice2Tx2.Inputs)
|
||||||
|
{
|
||||||
|
input.ScriptSig = Script.Empty; //Strip signatures
|
||||||
|
input.WitScript = WitScript.Empty; //Strip signatures
|
||||||
|
}
|
||||||
|
|
||||||
|
output = invoice2Tx2.Outputs.First(o =>
|
||||||
|
o.ScriptPubKey == invoice2Address.ScriptPubKey);
|
||||||
|
output.Value -= new Money(10_000, MoneyUnit.Satoshi);
|
||||||
|
output.ScriptPubKey = new Key().ScriptPubKey;
|
||||||
|
invoice2Tx2 = await tester.ExplorerNode.SignRawTransactionAsync(invoice2Tx2);
|
||||||
|
await tester.ExplorerNode.SendRawTransactionAsync(invoice2Tx2);
|
||||||
|
tester.ExplorerNode.Generate(1);
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id);
|
||||||
|
Assert.Equal(InvoiceStatus.New, i.Status);
|
||||||
|
Assert.Single(i.GetPayments());
|
||||||
|
Assert.False(i.GetPayments().First().Accounted);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ using NBXplorer.Models;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
using NBitcoin.Altcoins.Elements;
|
using NBitcoin.Altcoins.Elements;
|
||||||
|
using NBitcoin.RPC;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Bitcoin
|
namespace BTCPayServer.Payments.Bitcoin
|
||||||
{
|
{
|
||||||
|
@ -152,25 +153,25 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||||
});
|
});
|
||||||
foreach (var output in network.GetValidOutputs(evt))
|
foreach (var output in network.GetValidOutputs(evt))
|
||||||
{
|
{
|
||||||
var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant();
|
var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant();
|
||||||
var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new [] {key})).FirstOrDefault();
|
var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault();
|
||||||
if (invoice != null)
|
if (invoice != null)
|
||||||
|
{
|
||||||
|
var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy,
|
||||||
|
output.Item1.KeyPath, output.Item1.ScriptPubKey);
|
||||||
|
var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, evt.TransactionData.Transaction.RBF);
|
||||||
|
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
|
||||||
|
if (!alreadyExist)
|
||||||
{
|
{
|
||||||
var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy,
|
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network);
|
||||||
output.Item1.KeyPath, output.Item1.ScriptPubKey);
|
if (payment != null)
|
||||||
var paymentData = new BitcoinLikePaymentData(address, output.matchedOutput.Value, output.outPoint, evt.TransactionData.Transaction.RBF);
|
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
|
||||||
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
|
|
||||||
if (!alreadyExist)
|
|
||||||
{
|
|
||||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network);
|
|
||||||
if(payment != null)
|
|
||||||
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await UpdatePaymentStates(wallet, invoice.Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdatePaymentStates(wallet, invoice.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -216,7 +217,6 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||||
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
|
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
|
||||||
.Select(p => p.Outpoint.Hash)
|
.Select(p => p.Outpoint.Hash)
|
||||||
.ToArray());
|
.ToArray());
|
||||||
var conflicts = GetConflicts(transactions.Select(t => t.Value));
|
|
||||||
foreach (var payment in invoice.GetPayments(wallet.Network))
|
foreach (var payment in invoice.GetPayments(wallet.Network))
|
||||||
{
|
{
|
||||||
if (payment.GetPaymentMethodId().PaymentType != PaymentTypes.BTCLike)
|
if (payment.GetPaymentMethodId().PaymentType != PaymentTypes.BTCLike)
|
||||||
|
@ -225,8 +225,29 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||||
if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx))
|
if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx))
|
||||||
continue;
|
continue;
|
||||||
var txId = tx.Transaction.GetHash();
|
var txId = tx.Transaction.GetHash();
|
||||||
var txConflict = conflicts.GetConflict(txId);
|
bool accounted = true;
|
||||||
var accounted = txConflict == null || txConflict.IsWinner(txId);
|
if (tx.Confirmations == 0)
|
||||||
|
{
|
||||||
|
// Let's check if it was orphaned by broadcasting it again
|
||||||
|
var explorerClient = _ExplorerClients.GetExplorerClient(wallet.Network);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await explorerClient.BroadcastAsync(tx.Transaction, _Cts.Token);
|
||||||
|
accounted = result.Success ||
|
||||||
|
result.RPCCode == RPCErrorCode.RPC_TRANSACTION_ALREADY_IN_CHAIN ||
|
||||||
|
!(
|
||||||
|
// Happen if a blocks mined a replacement
|
||||||
|
// Or if the tx is a double spend of something already in the mempool without rbf
|
||||||
|
result.RPCCode == RPCErrorCode.RPC_TRANSACTION_ERROR ||
|
||||||
|
// Happen if RBF is on and fee insufficient
|
||||||
|
result.RPCCode == RPCErrorCode.RPC_TRANSACTION_REJECTED);
|
||||||
|
}
|
||||||
|
// RPC might be unavailable, we can't check double spend so let's assume there is none
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool updated = false;
|
bool updated = false;
|
||||||
if (accounted != payment.Accounted)
|
if (accounted != payment.Accounted)
|
||||||
|
@ -258,65 +279,6 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||||
return invoice;
|
return invoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TransactionConflict
|
|
||||||
{
|
|
||||||
public Dictionary<uint256, TransactionResult> Transactions { get; set; } = new Dictionary<uint256, TransactionResult>();
|
|
||||||
|
|
||||||
|
|
||||||
uint256 _Winner;
|
|
||||||
public bool IsWinner(uint256 txId)
|
|
||||||
{
|
|
||||||
if (_Winner == null)
|
|
||||||
{
|
|
||||||
var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1);
|
|
||||||
if (!confirmed.Equals(default(KeyValuePair<uint256, TransactionResult>)))
|
|
||||||
{
|
|
||||||
_Winner = confirmed.Key;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Take the most recent (bitcoin node would not forward a conflict without a successful RBF)
|
|
||||||
_Winner = Transactions
|
|
||||||
.OrderByDescending(t => t.Value.Timestamp)
|
|
||||||
.First()
|
|
||||||
.Key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _Winner == txId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class TransactionConflicts : List<TransactionConflict>
|
|
||||||
{
|
|
||||||
public TransactionConflicts(IEnumerable<TransactionConflict> collection) : base(collection)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public TransactionConflict GetConflict(uint256 txId)
|
|
||||||
{
|
|
||||||
return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private TransactionConflicts GetConflicts(IEnumerable<TransactionResult> transactions)
|
|
||||||
{
|
|
||||||
Dictionary<OutPoint, TransactionConflict> conflictsByOutpoint = new Dictionary<OutPoint, TransactionConflict>();
|
|
||||||
foreach (var tx in transactions)
|
|
||||||
{
|
|
||||||
var hash = tx.Transaction.GetHash();
|
|
||||||
foreach (var input in tx.Transaction.Inputs)
|
|
||||||
{
|
|
||||||
TransactionConflict conflict = new TransactionConflict();
|
|
||||||
if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict))
|
|
||||||
{
|
|
||||||
conflict = conflictsByOutpoint[input.PrevOut];
|
|
||||||
}
|
|
||||||
if (!conflict.Transactions.ContainsKey(hash))
|
|
||||||
conflict.Transactions.Add(hash, tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network)
|
private async Task<int> FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network)
|
||||||
{
|
{
|
||||||
int totalPayment = 0;
|
int totalPayment = 0;
|
||||||
|
@ -349,7 +311,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||||
if (payment != null)
|
if (payment != null)
|
||||||
{
|
{
|
||||||
invoice = await ReceivedPayment(wallet, invoice, payment, strategy);
|
invoice = await ReceivedPayment(wallet, invoice, payment, strategy);
|
||||||
if(invoice == null)
|
if (invoice == null)
|
||||||
continue;
|
continue;
|
||||||
totalPayment++;
|
totalPayment++;
|
||||||
}
|
}
|
||||||
|
@ -385,7 +347,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||||
invoice.SetPaymentMethod(paymentMethod);
|
invoice.SetPaymentMethod(paymentMethod);
|
||||||
}
|
}
|
||||||
wallet.InvalidateCache(strategy);
|
wallet.InvalidateCache(strategy);
|
||||||
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
|
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||||
return invoice;
|
return invoice;
|
||||||
}
|
}
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
|
Loading…
Add table
Reference in a new issue