From 42f208fad4647a4ddd5fe238f5ad2a5970c4ca60 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi <90936742+Reckless-Satoshi@users.noreply.github.com>
Date: Tue, 14 Mar 2023 17:23:11 +0000
Subject: [PATCH] Fix race condition onchain (#388)
* Fix race condition swaps
* Collect new phrases
* Increase random delay interval
---
api/lightning/node.py | 10 +++--
api/logics.py | 25 +++--------
api/management/commands/follow_invoices.py | 44 +++++++++++++++++--
api/models.py | 9 ++--
api/serializers.py | 2 +-
api/tasks.py | 1 +
api/views.py | 5 ++-
.../TradeBox/Prompts/Successful.tsx | 38 ++++++++++++++++
frontend/src/contexts/AppContext.tsx | 2 +-
frontend/src/models/Order.model.ts | 2 +
frontend/static/locales/ca.json | 1 +
frontend/static/locales/cs.json | 1 +
frontend/static/locales/de.json | 1 +
frontend/static/locales/en.json | 1 +
frontend/static/locales/es.json | 1 +
frontend/static/locales/eu.json | 1 +
frontend/static/locales/fr.json | 1 +
frontend/static/locales/it.json | 1 +
frontend/static/locales/pl.json | 1 +
frontend/static/locales/pt.json | 1 +
frontend/static/locales/ru.json | 1 +
frontend/static/locales/sv.json | 1 +
frontend/static/locales/th.json | 1 +
frontend/static/locales/zh-SI.json | 1 +
frontend/static/locales/zh-TR.json | 1 +
25 files changed, 120 insertions(+), 33 deletions(-)
diff --git a/api/lightning/node.py b/api/lightning/node.py
index 98a98ff6..b21a565e 100644
--- a/api/lightning/node.py
+++ b/api/lightning/node.py
@@ -1,6 +1,5 @@
import hashlib
import os
-import random
import secrets
import time
from base64 import b64decode
@@ -129,7 +128,7 @@ class LNNode:
}
@classmethod
- def pay_onchain(cls, onchainpayment, valid_code=1, on_mempool_code=2):
+ def pay_onchain(cls, onchainpayment, queue_code=5, on_mempool_code=2):
"""Send onchain transaction for buyer payouts"""
if config("DISABLE_ONCHAIN", cast=bool):
@@ -144,9 +143,12 @@ class LNNode:
)
# Cheap security measure to ensure there has been some non-deterministic time between request and DB check
- time.sleep(random.uniform(0.5, 10))
+ delay = (
+ secrets.randbelow(2**256) / (2**256) * 10
+ ) # Random uniform 0 to 5 secs with good entropy
+ time.sleep(3 + delay)
- if onchainpayment.status == valid_code:
+ if onchainpayment.status == queue_code:
# Changing the state to "MEMPO" should be atomic with SendCoins.
onchainpayment.status = on_mempool_code
onchainpayment.save()
diff --git a/api/logics.py b/api/logics.py
index 0763eda5..a85aaea9 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -481,9 +481,9 @@ class Logics:
"bad_request": "Only orders in dispute accept dispute statements"
}
- if len(statement) > 5000:
+ if len(statement) > 10000:
return False, {
- "bad_statement": "The statement is longer than 5000 characters"
+ "bad_statement": "The statement is longer than 10000 characters"
}
if len(statement) < 100:
@@ -554,7 +554,7 @@ class Logics:
confirmed = onchain_payment.balance.onchain_confirmed
reserve = 300000 # We assume a reserve of 300K Sats (3 times higher than LND's default anchor reserve)
pending_txs = OnchainPayment.objects.filter(
- status=OnchainPayment.Status.VALID
+ status__in=[OnchainPayment.Status.VALID, OnchainPayment.Status.QUEUE]
).aggregate(Sum("num_satoshis"))["num_satoshis__sum"]
if pending_txs is None:
@@ -1049,13 +1049,6 @@ class Logics:
safe_cltv_expiry_secs = cltv_expiry_secs * MAX_MINING_NETWORK_SPEEDUP_EXPECTED
# Convert to blocks using assummed average block time (~8 mins/block)
cltv_expiry_blocks = int(safe_cltv_expiry_secs / (BLOCK_TIME * 60))
- print(
- invoice_concept,
- " cltv_expiry_hours:",
- cltv_expiry_secs / 3600,
- " cltv_expiry_blocks:",
- cltv_expiry_blocks,
- )
return cltv_expiry_blocks
@@ -1442,20 +1435,16 @@ class Logics:
else:
if not order.payout_tx.status == OnchainPayment.Status.VALID:
return False
-
- valid = LNNode.pay_onchain(
- order.payout_tx,
- valid_code=OnchainPayment.Status.VALID,
- on_mempool_code=OnchainPayment.Status.MEMPO,
- )
- if valid:
+ else:
+ # Add onchain payment to queue
order.status = Order.Status.SUC
+ order.payout_tx.status = OnchainPayment.Status.QUEUE
+ order.payout_tx.save()
order.save()
send_message.delay(order.id, "trade_successful")
order.contract_finalization_time = timezone.now()
order.save()
return True
- return False
@classmethod
def confirm_fiat(cls, order, user):
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index dcefbea4..e7342fc5 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -8,7 +8,7 @@ from django.utils import timezone
from api.lightning.node import LNNode
from api.logics import Logics
-from api.models import LNPayment, Order
+from api.models import LNPayment, OnchainPayment, Order
from api.tasks import follow_send_payment, send_message
MACAROON = b64decode(config("LND_MACAROON_BASE64"))
@@ -16,7 +16,7 @@ MACAROON = b64decode(config("LND_MACAROON_BASE64"))
class Command(BaseCommand):
- help = "Follows all active hold invoices"
+ help = "Follows all active hold invoices, sends out payments"
rest = 5 # seconds between consecutive checks for invoice updates
def handle(self, *args, **options):
@@ -130,6 +130,14 @@ class Command(BaseCommand):
self.stdout.write(str(debug))
def send_payments(self):
+ """
+ Checks for invoices and onchain payments that are due to be paid.
+ Sends the payments.
+ """
+ self.send_ln_payments()
+ self.send_onchain_payments()
+
+ def send_ln_payments(self):
"""
Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts.
Checks if any payment is due for retry, and tries to pay it.
@@ -153,8 +161,36 @@ class Command(BaseCommand):
queryset = queryset.union(queryset_retries)
- for lnpayment in queryset:
- follow_send_payment(lnpayment.payment_hash)
+ if len(queryset) > 0:
+ for lnpayment in queryset:
+ follow_send_payment.delay(lnpayment.payment_hash)
+
+ def send_onchain_payments(self):
+
+ queryset = OnchainPayment.objects.filter(
+ status=OnchainPayment.Status.QUEUE,
+ )
+
+ if len(queryset) > 0:
+ for onchainpayment in queryset:
+ # Checks that this onchain payment is part of an order with a settled escrow
+ if not hasattr(onchainpayment, "order_paid_TX"):
+ self.stdout.write(
+ f"Onchain payment {str(onchainpayment)} has no parent order!"
+ )
+ return
+ order = onchainpayment.order_paid_TX
+ if order.trade_escrow.status == LNPayment.Status.SETLED:
+ # Sends out onchainpayment
+ LNNode.pay_onchain(
+ onchainpayment,
+ OnchainPayment.Status.QUEUE,
+ OnchainPayment.Status.MEMPO,
+ )
+ else:
+ self.stdout.write(
+ f"Onchain payment {str(onchainpayment)} for order {str(order)} escrow is not settled!"
+ )
def update_order_status(self, lnpayment):
"""Background process following LND hold invoices
diff --git a/api/models.py b/api/models.py
index 5e9ab41d..94cc0f6c 100644
--- a/api/models.py
+++ b/api/models.py
@@ -185,10 +185,11 @@ class OnchainPayment(models.Model):
class Status(models.IntegerChoices):
CREAT = 0, "Created" # User was given platform fees and suggested mining fees
- VALID = 1, "Valid" # Valid onchain address submitted
+ VALID = 1, "Valid" # Valid onchain address and fee submitted
MEMPO = 2, "In mempool" # Tx is sent to mempool
- CONFI = 3, "Confirmed" # Tx is confirme +2 blocks
+ CONFI = 3, "Confirmed" # Tx is confirmed +2 blocks
CANCE = 4, "Cancelled" # Cancelled tx
+ QUEUE = 5, "Queued" # Payment is queued to be sent out
def get_balance():
balance = BalanceLog.objects.create()
@@ -441,10 +442,10 @@ class Order(models.Model):
# in dispute
is_disputed = models.BooleanField(default=False, null=False)
maker_statement = models.TextField(
- max_length=5000, null=True, default=None, blank=True
+ max_length=10000, null=True, default=None, blank=True
)
taker_statement = models.TextField(
- max_length=5000, null=True, default=None, blank=True
+ max_length=10000, null=True, default=None, blank=True
)
# LNpayments
diff --git a/api/serializers.py b/api/serializers.py
index ded1e039..6124d0e4 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -501,7 +501,7 @@ class UpdateOrderSerializer(serializers.Serializer):
max_length=100, allow_null=True, allow_blank=True, default=None
)
statement = serializers.CharField(
- max_length=10000, allow_null=True, allow_blank=True, default=None
+ max_length=11000, allow_null=True, allow_blank=True, default=None
)
action = serializers.ChoiceField(
choices=(
diff --git a/api/tasks.py b/api/tasks.py
index 4838e060..d53533b4 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -97,6 +97,7 @@ def follow_send_payment(hash):
payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat,
timeout_seconds=timeout_seconds,
+ allow_self_payment=True,
)
order = lnpayment.order_paid_LN
diff --git a/api/views.py b/api/views.py
index eb4dcc9b..894589e6 100644
--- a/api/views.py
+++ b/api/views.py
@@ -471,12 +471,15 @@ class OrderView(viewsets.ViewSet):
if order.is_swap:
data["num_satoshis"] = order.payout_tx.num_satoshis
data["sent_satoshis"] = order.payout_tx.sent_satoshis
+ data["network"] = str(config("NETWORK"))
if order.payout_tx.status in [
OnchainPayment.Status.MEMPO,
OnchainPayment.Status.CONFI,
]:
data["txid"] = order.payout_tx.txid
- data["network"] = str(config("NETWORK"))
+ elif order.payout_tx.status == OnchainPayment.Status.QUEUE:
+ data["tx_queued"] = True
+ data["address"] = order.payout_tx.address
return Response(data, status.HTTP_200_OK)
diff --git a/frontend/src/components/TradeBox/Prompts/Successful.tsx b/frontend/src/components/TradeBox/Prompts/Successful.tsx
index b6c077f4..9bb97c61 100644
--- a/frontend/src/components/TradeBox/Prompts/Successful.tsx
+++ b/frontend/src/components/TradeBox/Prompts/Successful.tsx
@@ -11,6 +11,7 @@ import {
Tooltip,
IconButton,
Button,
+ CircularProgress,
} from '@mui/material';
import currencies from '../../../../static/assets/currencies.json';
import TradeSummary from '../TradeSummary';
@@ -150,6 +151,43 @@ export const SuccessfulPrompt = ({
+
+
+
+
+
+ {t('Sending coins to')}
+
+ {
+ systemClient.copyToClipboard(order.address);
+ }}
+ >
+
+
+
+
+
+
+ {order.address}
+
+
+
+
+