|
|
|
|
@ -1,7 +1,7 @@
|
|
|
|
|
import asyncio |
|
|
|
|
import json |
|
|
|
|
import os |
|
|
|
|
from typing import TYPE_CHECKING, Optional, Dict, Union |
|
|
|
|
from typing import TYPE_CHECKING, Optional, Dict, Union, Sequence, Tuple |
|
|
|
|
from decimal import Decimal |
|
|
|
|
import math |
|
|
|
|
import time |
|
|
|
|
@ -9,6 +9,7 @@ import time
|
|
|
|
|
import attr |
|
|
|
|
import aiohttp |
|
|
|
|
|
|
|
|
|
from . import lnutil |
|
|
|
|
from .crypto import sha256, hash_160 |
|
|
|
|
from .ecc import ECPrivkey |
|
|
|
|
from .bitcoin import (script_to_p2wsh, opcodes, p2wsh_nested_script, push_script, |
|
|
|
|
@ -38,6 +39,7 @@ if TYPE_CHECKING:
|
|
|
|
|
from .wallet import Abstract_Wallet |
|
|
|
|
from .lnwatcher import LNWalletWatcher |
|
|
|
|
from .lnworker import LNWallet |
|
|
|
|
from .lnchannel import Channel |
|
|
|
|
from .simple_config import SimpleConfig |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -76,7 +78,16 @@ WITNESS_TEMPLATE_REVERSE_SWAP = [
|
|
|
|
|
opcodes.OP_CHECKSIG |
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
def check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, *, refund_pubkey=None, claim_pubkey=None): |
|
|
|
|
|
|
|
|
|
def check_reverse_redeem_script( |
|
|
|
|
*, |
|
|
|
|
redeem_script: str, |
|
|
|
|
lockup_address: str, |
|
|
|
|
payment_hash: bytes, |
|
|
|
|
locktime: int, |
|
|
|
|
refund_pubkey: bytes = None, |
|
|
|
|
claim_pubkey: bytes = None, |
|
|
|
|
) -> None: |
|
|
|
|
redeem_script = bytes.fromhex(redeem_script) |
|
|
|
|
parsed_script = [x for x in script_GetOp(redeem_script)] |
|
|
|
|
if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP): |
|
|
|
|
@ -91,7 +102,6 @@ def check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, loc
|
|
|
|
|
raise Exception("rswap check failed: our pubkey not in script") |
|
|
|
|
if locktime != int.from_bytes(parsed_script[10][1], byteorder='little'): |
|
|
|
|
raise Exception("rswap check failed: inconsistent locktime and script") |
|
|
|
|
return parsed_script[7][1], parsed_script[13][1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SwapServerError(Exception): |
|
|
|
|
@ -109,7 +119,7 @@ class SwapData(StoredObject):
|
|
|
|
|
onchain_amount = attr.ib(type=int) # in sats |
|
|
|
|
lightning_amount = attr.ib(type=int) # in sats |
|
|
|
|
redeem_script = attr.ib(type=bytes, converter=hex_to_bytes) |
|
|
|
|
preimage = attr.ib(type=bytes, converter=hex_to_bytes) |
|
|
|
|
preimage = attr.ib(type=Optional[bytes], converter=hex_to_bytes) |
|
|
|
|
prepay_hash = attr.ib(type=Optional[bytes], converter=hex_to_bytes) |
|
|
|
|
privkey = attr.ib(type=bytes, converter=hex_to_bytes) |
|
|
|
|
lockup_address = attr.ib(type=str) |
|
|
|
|
@ -349,6 +359,7 @@ class SwapManager(Logger):
|
|
|
|
|
return self.get_fee(CLAIM_FEE_SIZE) |
|
|
|
|
|
|
|
|
|
def get_fee(self, size): |
|
|
|
|
# note: 'size' is in vbytes |
|
|
|
|
return self._get_fee(size=size, config=self.wallet.config) |
|
|
|
|
|
|
|
|
|
@classmethod |
|
|
|
|
@ -376,15 +387,16 @@ class SwapManager(Logger):
|
|
|
|
|
if swap.funding_txid is None: |
|
|
|
|
password = self.wallet.get_unlocked_password() |
|
|
|
|
for batch_rbf in [True, False]: |
|
|
|
|
tx = self.create_funding_tx(swap, None, password, batch_rbf=batch_rbf) |
|
|
|
|
tx = self.create_funding_tx(swap, None, password=password, batch_rbf=batch_rbf) |
|
|
|
|
try: |
|
|
|
|
await self.broadcast_funding_tx(swap, tx) |
|
|
|
|
except TxBroadcastServerReturnedError: |
|
|
|
|
continue |
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
def create_normal_swap(self, *, lightning_amount_sat=None, payment_hash: bytes=None, their_pubkey=None): |
|
|
|
|
def create_normal_swap(self, *, lightning_amount_sat: int, payment_hash: bytes, their_pubkey: bytes = None): |
|
|
|
|
""" server method """ |
|
|
|
|
assert lightning_amount_sat |
|
|
|
|
locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND |
|
|
|
|
our_privkey = os.urandom(32) |
|
|
|
|
our_pubkey = ECPrivkey(our_privkey).get_public_key_bytes(compressed=True) |
|
|
|
|
@ -400,8 +412,6 @@ class SwapManager(Logger):
|
|
|
|
|
lightning_amount_sat=lightning_amount_sat, |
|
|
|
|
payment_hash=payment_hash, |
|
|
|
|
our_privkey=our_privkey, |
|
|
|
|
their_pubkey=their_pubkey, |
|
|
|
|
invoice=None, |
|
|
|
|
prepay=True, |
|
|
|
|
) |
|
|
|
|
self.lnworker.register_hold_invoice(payment_hash, self.hold_invoice_callback) |
|
|
|
|
@ -409,35 +419,32 @@ class SwapManager(Logger):
|
|
|
|
|
|
|
|
|
|
def add_normal_swap( |
|
|
|
|
self, *, |
|
|
|
|
redeem_script=None, |
|
|
|
|
locktime=None, |
|
|
|
|
onchain_amount_sat=None, |
|
|
|
|
lightning_amount_sat=None, |
|
|
|
|
payment_hash=None, |
|
|
|
|
our_privkey=None, |
|
|
|
|
their_pubkey=None, |
|
|
|
|
invoice=None, |
|
|
|
|
prepay=None, |
|
|
|
|
channels=None, |
|
|
|
|
): |
|
|
|
|
""" if invoice is None, create a hold invoice """ |
|
|
|
|
redeem_script: str, |
|
|
|
|
locktime: int, # onchain |
|
|
|
|
onchain_amount_sat: int, |
|
|
|
|
lightning_amount_sat: int, |
|
|
|
|
payment_hash: bytes, |
|
|
|
|
our_privkey: bytes, |
|
|
|
|
prepay: bool, |
|
|
|
|
channels: Optional[Sequence['Channel']] = None, |
|
|
|
|
) -> Tuple[SwapData, str, str]: |
|
|
|
|
"""creates a hold invoice""" |
|
|
|
|
if prepay: |
|
|
|
|
prepay_amount_sat = self.get_claim_fee() * 2 |
|
|
|
|
invoice_amount_sat = lightning_amount_sat - prepay_amount_sat |
|
|
|
|
else: |
|
|
|
|
invoice_amount_sat = lightning_amount_sat |
|
|
|
|
|
|
|
|
|
if not invoice: |
|
|
|
|
_, invoice = self.lnworker.get_bolt11_invoice( |
|
|
|
|
payment_hash=payment_hash, |
|
|
|
|
amount_msat=invoice_amount_sat * 1000, |
|
|
|
|
message='Submarine swap', |
|
|
|
|
expiry=300, |
|
|
|
|
fallback_address=None, |
|
|
|
|
channels=channels, |
|
|
|
|
) |
|
|
|
|
# add payment info to lnworker |
|
|
|
|
self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat) |
|
|
|
|
_, invoice = self.lnworker.get_bolt11_invoice( |
|
|
|
|
payment_hash=payment_hash, |
|
|
|
|
amount_msat=invoice_amount_sat * 1000, |
|
|
|
|
message='Submarine swap', |
|
|
|
|
expiry=300, |
|
|
|
|
fallback_address=None, |
|
|
|
|
channels=channels, |
|
|
|
|
) |
|
|
|
|
# add payment info to lnworker |
|
|
|
|
self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat) |
|
|
|
|
|
|
|
|
|
if prepay: |
|
|
|
|
prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000) |
|
|
|
|
@ -477,14 +484,14 @@ class SwapManager(Logger):
|
|
|
|
|
self.add_lnwatcher_callback(swap) |
|
|
|
|
return swap, invoice, prepay_invoice |
|
|
|
|
|
|
|
|
|
def create_reverse_swap(self, *, lightning_amount_sat=None, their_pubkey=None): |
|
|
|
|
def create_reverse_swap(self, *, lightning_amount_sat: int, their_pubkey: bytes) -> SwapData: |
|
|
|
|
""" server method. """ |
|
|
|
|
assert lightning_amount_sat is not None |
|
|
|
|
locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND |
|
|
|
|
privkey = os.urandom(32) |
|
|
|
|
our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) |
|
|
|
|
onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False) |
|
|
|
|
preimage = os.urandom(32) |
|
|
|
|
assert lightning_amount_sat is not None |
|
|
|
|
payment_hash = sha256(preimage) |
|
|
|
|
redeem_script = construct_script( |
|
|
|
|
WITNESS_TEMPLATE_REVERSE_SWAP, |
|
|
|
|
@ -501,7 +508,18 @@ class SwapManager(Logger):
|
|
|
|
|
lightning_amount_sat=lightning_amount_sat) |
|
|
|
|
return swap |
|
|
|
|
|
|
|
|
|
def add_reverse_swap(self, *, redeem_script=None, locktime=None, privkey=None, lightning_amount_sat=None, onchain_amount_sat=None, preimage=None, payment_hash=None, prepay_hash=None): |
|
|
|
|
def add_reverse_swap( |
|
|
|
|
self, |
|
|
|
|
*, |
|
|
|
|
redeem_script: str, |
|
|
|
|
locktime: int, # onchain |
|
|
|
|
privkey: bytes, |
|
|
|
|
lightning_amount_sat: int, |
|
|
|
|
onchain_amount_sat: int, |
|
|
|
|
preimage: bytes, |
|
|
|
|
payment_hash: bytes, |
|
|
|
|
prepay_hash: Optional[bytes] = None, |
|
|
|
|
) -> SwapData: |
|
|
|
|
lockup_address = script_to_p2wsh(redeem_script) |
|
|
|
|
receive_address = self.wallet.get_receiving_address() |
|
|
|
|
swap = SwapData( |
|
|
|
|
@ -526,7 +544,7 @@ class SwapManager(Logger):
|
|
|
|
|
self.add_lnwatcher_callback(swap) |
|
|
|
|
return swap |
|
|
|
|
|
|
|
|
|
def add_invoice(self, invoice, pay_now=False): |
|
|
|
|
def add_invoice(self, invoice: str, pay_now: bool = False) -> None: |
|
|
|
|
invoice = Invoice.from_bech32(invoice) |
|
|
|
|
key = invoice.rhash |
|
|
|
|
payment_hash = bytes.fromhex(key) |
|
|
|
|
@ -548,28 +566,41 @@ class SwapManager(Logger):
|
|
|
|
|
password, |
|
|
|
|
tx: PartialTransaction = None, |
|
|
|
|
channels = None, |
|
|
|
|
) -> str: |
|
|
|
|
) -> Optional[str]: |
|
|
|
|
"""send on-chain BTC, receive on Lightning |
|
|
|
|
|
|
|
|
|
Old (removed) flow: |
|
|
|
|
- User generates an LN invoice with RHASH, and knows preimage. |
|
|
|
|
- User creates on-chain output locked to RHASH. |
|
|
|
|
- Server pays LN invoice. User reveals preimage. |
|
|
|
|
- Server spends the on-chain output using preimage. |
|
|
|
|
|
|
|
|
|
New flow: |
|
|
|
|
- user requests swap |
|
|
|
|
- server creates preimage, sends RHASH to user |
|
|
|
|
- user creates hold invoice, sends it to server |
|
|
|
|
|
|
|
|
|
- User requests swap |
|
|
|
|
- Server creates preimage, sends RHASH to user |
|
|
|
|
- User creates hold invoice, sends it to server |
|
|
|
|
- Server sends HTLC, user holds it |
|
|
|
|
- User creates on-chain output locked to RHASH |
|
|
|
|
- Server spends the on-chain output using preimage (revealing the preimage) |
|
|
|
|
- User fulfills HTLC using preimage |
|
|
|
|
""" |
|
|
|
|
assert self.network |
|
|
|
|
assert self.lnwatcher |
|
|
|
|
swap, invoice = await self.request_normal_swap(lightning_amount_sat, expected_onchain_amount_sat, channels=channels) |
|
|
|
|
tx = self.create_funding_tx(swap, tx, password) |
|
|
|
|
return await self.wait_for_htlcs_and_broadcast(swap, invoice, tx) |
|
|
|
|
swap, invoice = await self.request_normal_swap( |
|
|
|
|
lightning_amount_sat=lightning_amount_sat, |
|
|
|
|
expected_onchain_amount_sat=expected_onchain_amount_sat, |
|
|
|
|
channels=channels, |
|
|
|
|
) |
|
|
|
|
tx = self.create_funding_tx(swap, tx, password=password) |
|
|
|
|
return await self.wait_for_htlcs_and_broadcast(swap=swap, invoice=invoice, tx=tx) |
|
|
|
|
|
|
|
|
|
async def request_normal_swap(self, lightning_amount_sat, expected_onchain_amount_sat, channels=None): |
|
|
|
|
amount_msat = lightning_amount_sat * 1000 |
|
|
|
|
async def request_normal_swap( |
|
|
|
|
self, |
|
|
|
|
*, |
|
|
|
|
lightning_amount_sat: int, |
|
|
|
|
expected_onchain_amount_sat: int, |
|
|
|
|
channels: Optional[Sequence['Channel']] = None, |
|
|
|
|
) -> Tuple[SwapData, str]: |
|
|
|
|
refund_privkey = os.urandom(32) |
|
|
|
|
refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True) |
|
|
|
|
|
|
|
|
|
@ -585,8 +616,6 @@ class SwapManager(Logger):
|
|
|
|
|
timeout=30) |
|
|
|
|
data = json.loads(response) |
|
|
|
|
payment_hash = bytes.fromhex(data["preimageHash"]) |
|
|
|
|
preimage = None |
|
|
|
|
invoice = None |
|
|
|
|
|
|
|
|
|
zeroconf = data["acceptZeroConf"] |
|
|
|
|
onchain_amount = data["expectedAmount"] |
|
|
|
|
@ -594,7 +623,13 @@ class SwapManager(Logger):
|
|
|
|
|
lockup_address = data["address"] |
|
|
|
|
redeem_script = data["redeemScript"] |
|
|
|
|
# verify redeem_script is built with our pubkey and preimage |
|
|
|
|
claim_pubkey, _ = check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey) |
|
|
|
|
check_reverse_redeem_script( |
|
|
|
|
redeem_script=redeem_script, |
|
|
|
|
lockup_address=lockup_address, |
|
|
|
|
payment_hash=payment_hash, |
|
|
|
|
locktime=locktime, |
|
|
|
|
refund_pubkey=refund_pubkey, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
# check that onchain_amount is not more than what we estimated |
|
|
|
|
if onchain_amount > expected_onchain_amount_sat: |
|
|
|
|
@ -611,14 +646,18 @@ class SwapManager(Logger):
|
|
|
|
|
onchain_amount_sat=onchain_amount, |
|
|
|
|
payment_hash=payment_hash, |
|
|
|
|
our_privkey=refund_privkey, |
|
|
|
|
their_pubkey=claim_pubkey, |
|
|
|
|
invoice=invoice, |
|
|
|
|
prepay=False, |
|
|
|
|
channels=channels, |
|
|
|
|
) |
|
|
|
|
return swap, invoice |
|
|
|
|
|
|
|
|
|
async def wait_for_htlcs_and_broadcast(self, swap, invoice, tx): |
|
|
|
|
async def wait_for_htlcs_and_broadcast( |
|
|
|
|
self, |
|
|
|
|
*, |
|
|
|
|
swap: SwapData, |
|
|
|
|
invoice: str, |
|
|
|
|
tx: Transaction, |
|
|
|
|
) -> Optional[str]: |
|
|
|
|
payment_hash = swap.payment_hash |
|
|
|
|
refund_pubkey = ECPrivkey(swap.privkey).get_public_key_bytes(compressed=True) |
|
|
|
|
async def callback(payment_hash): |
|
|
|
|
@ -644,7 +683,14 @@ class SwapManager(Logger):
|
|
|
|
|
await asyncio.sleep(0.1) |
|
|
|
|
return swap.funding_txid |
|
|
|
|
|
|
|
|
|
def create_funding_tx(self, swap, tx, password, *, batch_rbf: Optional[bool] = None): |
|
|
|
|
def create_funding_tx( |
|
|
|
|
self, |
|
|
|
|
swap: SwapData, |
|
|
|
|
tx: Optional[PartialTransaction], |
|
|
|
|
*, |
|
|
|
|
password, |
|
|
|
|
batch_rbf: Optional[bool] = None, |
|
|
|
|
) -> PartialTransaction: |
|
|
|
|
# create funding tx |
|
|
|
|
# note: rbf must not decrease payment |
|
|
|
|
# this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output |
|
|
|
|
@ -663,7 +709,7 @@ class SwapManager(Logger):
|
|
|
|
|
return tx |
|
|
|
|
|
|
|
|
|
@log_exceptions |
|
|
|
|
async def request_swap_for_tx(self, tx: 'PartialTransaction'): |
|
|
|
|
async def request_swap_for_tx(self, tx: 'PartialTransaction') -> Optional[Tuple[SwapData, str, PartialTransaction]]: |
|
|
|
|
for o in tx.outputs(): |
|
|
|
|
if o.address == self.dummy_address: |
|
|
|
|
change_amount = o.value |
|
|
|
|
@ -679,7 +725,7 @@ class SwapManager(Logger):
|
|
|
|
|
return swap, invoice, tx |
|
|
|
|
|
|
|
|
|
@log_exceptions |
|
|
|
|
async def broadcast_funding_tx(self, swap, tx): |
|
|
|
|
async def broadcast_funding_tx(self, swap: SwapData, tx: Transaction) -> None: |
|
|
|
|
swap.funding_txid = tx.txid() |
|
|
|
|
await self.network.broadcast_transaction(tx) |
|
|
|
|
|
|
|
|
|
@ -688,7 +734,7 @@ class SwapManager(Logger):
|
|
|
|
|
*, |
|
|
|
|
lightning_amount_sat: int, |
|
|
|
|
expected_onchain_amount_sat: int, |
|
|
|
|
channels = None, |
|
|
|
|
channels: Optional[Sequence['Channel']] = None, |
|
|
|
|
) -> Optional[str]: |
|
|
|
|
"""send on Lightning, receive on-chain |
|
|
|
|
|
|
|
|
|
@ -729,7 +775,14 @@ class SwapManager(Logger):
|
|
|
|
|
onchain_amount = data["onchainAmount"] |
|
|
|
|
response_id = data['id'] |
|
|
|
|
# verify redeem_script is built with our pubkey and preimage |
|
|
|
|
check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=None, claim_pubkey=our_pubkey) |
|
|
|
|
check_reverse_redeem_script( |
|
|
|
|
redeem_script=redeem_script, |
|
|
|
|
lockup_address=lockup_address, |
|
|
|
|
payment_hash=payment_hash, |
|
|
|
|
locktime=locktime, |
|
|
|
|
refund_pubkey=None, |
|
|
|
|
claim_pubkey=our_pubkey, |
|
|
|
|
) |
|
|
|
|
# check that the onchain amount is what we expected |
|
|
|
|
if onchain_amount < expected_onchain_amount_sat: |
|
|
|
|
raise Exception(f"rswap check failed: onchain_amount is less than what we expected: " |
|
|
|
|
|