Browse Source

New flow for submarine swaps:

- client requests payment_hash from the server
 - client sends an invoice with that hash
 - client waits to receive HTLCs, then broadcasts funding tx

This means that we now use same script for normal and reverse swaps.
The new flow is enabled by setting option LIGHTNING_SWAP_HTLC_FIRST
in the client. The old protocol is still supported server-side.
master
ThomasV 2 years ago
parent
commit
fd10ae3a3b
  1. 51
      electrum/plugins/swapserver/server.py
  2. 1
      electrum/plugins/swapserver/swapserver.py
  3. 1
      electrum/simple_config.py
  4. 396
      electrum/submarine_swaps.py

51
electrum/plugins/swapserver/server.py

@ -5,11 +5,10 @@ from collections import defaultdict
from aiohttp import web from aiohttp import web
from aiorpcx import NetAddress from aiorpcx import NetAddress
from electrum.util import log_exceptions, ignore_exceptions from electrum.util import log_exceptions, ignore_exceptions
from electrum.logging import Logger from electrum.logging import Logger
from electrum.util import EventListener from electrum.util import EventListener
from electrum.lnaddr import lndecode
class SwapServer(Logger, EventListener): class SwapServer(Logger, EventListener):
""" """
@ -24,6 +23,7 @@ class SwapServer(Logger, EventListener):
Logger.__init__(self) Logger.__init__(self)
self.config = config self.config = config
self.wallet = wallet self.wallet = wallet
self.sm = self.wallet.lnworker.swap_manager
self.addr = NetAddress.from_string(self.config.SWAPSERVER_ADDRESS) self.addr = NetAddress.from_string(self.config.SWAPSERVER_ADDRESS)
self.register_callbacks() # eventlistener self.register_callbacks() # eventlistener
@ -36,6 +36,8 @@ class SwapServer(Logger, EventListener):
app = web.Application() app = web.Application()
app.add_routes([web.get('/api/getpairs', self.get_pairs)]) app.add_routes([web.get('/api/getpairs', self.get_pairs)])
app.add_routes([web.post('/api/createswap', self.create_swap)]) app.add_routes([web.post('/api/createswap', self.create_swap)])
app.add_routes([web.post('/api/createnormalswap', self.create_normal_swap)])
app.add_routes([web.post('/api/addswapinvoice', self.add_swap_invoice)])
runner = web.AppRunner(app) runner = web.AppRunner(app)
await runner.setup() await runner.setup()
@ -44,7 +46,7 @@ class SwapServer(Logger, EventListener):
self.logger.info(f"now running and listening. addr={self.addr}") self.logger.info(f"now running and listening. addr={self.addr}")
async def get_pairs(self, r): async def get_pairs(self, r):
sm = self.wallet.lnworker.swap_manager sm = self.sm
sm.init_pairs() sm.init_pairs()
pairs = { pairs = {
"info": [], "info": [],
@ -84,9 +86,36 @@ class SwapServer(Logger, EventListener):
} }
return web.json_response(pairs) return web.json_response(pairs)
async def add_swap_invoice(self, r):
request = await r.json()
invoice = request['invoice']
self.sm.add_invoice(invoice, pay_now=True)
return web.json_response({})
async def create_normal_swap(self, r):
# normal for client, reverse for server
request = await r.json()
lightning_amount_sat = request['invoiceAmount']
their_pubkey = bytes.fromhex(request['refundPublicKey'])
assert len(their_pubkey) == 33
swap = self.sm.create_reverse_swap(
payment_hash=None,
lightning_amount_sat=lightning_amount_sat,
their_pubkey=their_pubkey
)
response = {
"id": swap.payment_hash.hex(),
'preimageHash': swap.payment_hash.hex(),
"acceptZeroConf": False,
"expectedAmount": swap.onchain_amount,
"timeoutBlockHeight": swap.locktime,
"address": swap.lockup_address,
"redeemScript": swap.redeem_script.hex(),
}
return web.json_response(response)
async def create_swap(self, r): async def create_swap(self, r):
sm = self.wallet.lnworker.swap_manager self.sm.init_pairs()
sm.init_pairs()
request = await r.json() request = await r.json()
req_type = request['type'] req_type = request['type']
assert request['pairId'] == 'BTC/BTC' assert request['pairId'] == 'BTC/BTC'
@ -96,7 +125,7 @@ class SwapServer(Logger, EventListener):
their_pubkey=bytes.fromhex(request['claimPublicKey']) their_pubkey=bytes.fromhex(request['claimPublicKey'])
assert len(payment_hash) == 32 assert len(payment_hash) == 32
assert len(their_pubkey) == 33 assert len(their_pubkey) == 33
swap, payment_hash, invoice, prepay_invoice = sm.add_server_swap( swap, invoice, prepay_invoice = self.sm.create_normal_swap(
lightning_amount_sat=lightning_amount_sat, lightning_amount_sat=lightning_amount_sat,
payment_hash=payment_hash, payment_hash=payment_hash,
their_pubkey=their_pubkey their_pubkey=their_pubkey
@ -111,13 +140,19 @@ class SwapServer(Logger, EventListener):
"onchainAmount": swap.onchain_amount, "onchainAmount": swap.onchain_amount,
} }
elif req_type == 'submarine': elif req_type == 'submarine':
# old protocol
their_invoice=request['invoice'] their_invoice=request['invoice']
their_pubkey=bytes.fromhex(request['refundPublicKey']) their_pubkey=bytes.fromhex(request['refundPublicKey'])
assert len(their_pubkey) == 33 assert len(their_pubkey) == 33
swap, payment_hash, invoice, prepay_invoice = sm.add_server_swap( lnaddr = lndecode(their_invoice)
invoice=their_invoice, payment_hash = lnaddr.paymenthash
lightning_amount_sat = int(lnaddr.get_amount_sat()) # should return int
swap = self.sm.create_reverse_swap(
lightning_amount_sat=lightning_amount_sat,
payment_hash=payment_hash,
their_pubkey=their_pubkey their_pubkey=their_pubkey
) )
self.sm.add_invoice(their_invoice, pay_now=False)
response = { response = {
"id": payment_hash.hex(), "id": payment_hash.hex(),
"acceptZeroConf": False, "acceptZeroConf": False,

1
electrum/plugins/swapserver/swapserver.py

@ -55,7 +55,6 @@ class SwapServerPlugin(BasePlugin):
self.server = SwapServer(self.config, wallet) self.server = SwapServer(self.config, wallet)
sm = wallet.lnworker.swap_manager sm = wallet.lnworker.swap_manager
for coro in [ for coro in [
sm.pay_pending_invoices(), # FIXME this method can raise, which is not properly handled...?
self.server.run(), self.server.run(),
]: ]:
asyncio.run_coroutine_threadsafe(daemon.taskgroup.spawn(coro), daemon.asyncio_loop) asyncio.run_coroutine_threadsafe(daemon.taskgroup.spawn(coro), daemon.asyncio_loop)

1
electrum/simple_config.py

@ -890,6 +890,7 @@ class SimpleConfig(Logger):
LIGHTNING_USE_GOSSIP = ConfigVar('use_gossip', default=False, type_=bool) LIGHTNING_USE_GOSSIP = ConfigVar('use_gossip', default=False, type_=bool)
LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar('use_recoverable_channels', default=True, type_=bool) LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar('use_recoverable_channels', default=True, type_=bool)
LIGHTNING_ALLOW_INSTANT_SWAPS = ConfigVar('allow_instant_swaps', default=False, type_=bool) LIGHTNING_ALLOW_INSTANT_SWAPS = ConfigVar('allow_instant_swaps', default=False, type_=bool)
LIGHTNING_SWAP_HTLC_FIRST = ConfigVar('swap_htlc_first', default=False, type_=bool)
LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int) LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int)
LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int) LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int)

396
electrum/submarine_swaps.py

@ -24,6 +24,10 @@ from . import constants
from .address_synchronizer import TX_HEIGHT_LOCAL from .address_synchronizer import TX_HEIGHT_LOCAL
from .i18n import _ from .i18n import _
from .bitcoin import construct_script
from .crypto import ripemd
from .invoices import Invoice
if TYPE_CHECKING: if TYPE_CHECKING:
from .network import Network from .network import Network
from .wallet import Abstract_Wallet from .wallet import Abstract_Wallet
@ -81,6 +85,40 @@ WITNESS_TEMPLATE_REVERSE_SWAP = [
opcodes.OP_CHECKSIG opcodes.OP_CHECKSIG
] ]
def check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, *, refund_pubkey=None, claim_pubkey=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):
raise Exception("rswap check failed: scriptcode does not match template")
if script_to_p2wsh(redeem_script.hex()) != lockup_address:
raise Exception("rswap check failed: inconsistent scriptcode and address")
if ripemd(payment_hash) != parsed_script[5][1]:
raise Exception("rswap check failed: our preimage not in script")
if claim_pubkey and claim_pubkey != parsed_script[7][1]:
raise Exception("rswap check failed: our pubkey not in script")
if refund_pubkey and refund_pubkey != parsed_script[13][1]:
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]
def check_normal_redeem_script(redeem_script, lockup_address, payment_hash, locktime, *, refund_pubkey=None, claim_pubkey=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_SWAP):
raise Exception("fswap check failed: scriptcode does not match template")
if script_to_p2wsh(redeem_script.hex()) != lockup_address:
raise Exception("fswap check failed: inconsistent scriptcode and address")
if ripemd(payment_hash) != parsed_script[1][1]:
raise Exception("fswap check failed: our preimage not in script")
if claim_pubkey and claim_pubkey != parsed_script[4][1]:
raise Exception("fswap check failed: our pubkey not in script")
if refund_pubkey and refund_pubkey != parsed_script[9][1]:
raise Exception("fswap check failed: our pubkey not in script")
if locktime != int.from_bytes(parsed_script[6][1], byteorder='little'):
raise Exception("fswap check failed: inconsistent locktime and script")
return parsed_script[4][1], parsed_script[9][1]
class SwapServerError(Exception): class SwapServerError(Exception):
def __str__(self): def __str__(self):
@ -174,9 +212,11 @@ class SwapManager(Logger):
if swap.is_redeemed: if swap.is_redeemed:
continue continue
self.add_lnwatcher_callback(swap) self.add_lnwatcher_callback(swap)
coro = self.pay_pending_invoices()
asyncio.run_coroutine_threadsafe(network.taskgroup.spawn(coro), network.asyncio_loop)
async def pay_pending_invoices(self): async def pay_pending_invoices(self):
# for server # FIXME this method can raise, which is not properly handled...?
self.invoices_to_pay = set() self.invoices_to_pay = set()
while True: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
@ -302,85 +342,164 @@ class SwapManager(Logger):
self.lnwatcher.add_callback(swap.lockup_address, callback) self.lnwatcher.add_callback(swap.lockup_address, callback)
async def hold_invoice_callback(self, payment_hash): async def hold_invoice_callback(self, payment_hash):
# note: this assumes the keystore is not encrypted
key = payment_hash.hex() key = payment_hash.hex()
if key in self.swaps: if key in self.swaps:
swap = self.swaps[key] swap = self.swaps[key]
if swap.funding_txid is None: if swap.funding_txid is None:
await self.start_normal_swap(swap, None, None) await self.broadcast_funding_tx(swap, None, None)
def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoice=None, their_pubkey=None):
from .bitcoin import construct_script
from .crypto import ripemd
from .lnaddr import lndecode
from .invoices import Invoice
def create_normal_swap(self, *, lightning_amount_sat=None, payment_hash=None, their_pubkey=None):
""" server method """
locktime = self.network.get_local_height() + 140 locktime = self.network.get_local_height() + 140
privkey = os.urandom(32) our_privkey = os.urandom(32)
our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) our_pubkey = ECPrivkey(our_privkey).get_public_key_bytes(compressed=True)
is_reverse_for_server = (invoice is not None) onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) # what the client is going to receive
if is_reverse_for_server: redeem_script = construct_script(
# client is doing a normal swap WITNESS_TEMPLATE_REVERSE_SWAP,
lnaddr = lndecode(invoice) {1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey}
payment_hash = lnaddr.paymenthash )
lightning_amount_sat = int(lnaddr.get_amount_sat()) # should return int return self.add_normal_swap(
onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False) redeem_script=redeem_script,
redeem_script = construct_script( locktime=locktime,
WITNESS_TEMPLATE_SWAP, onchain_amount_sat=onchain_amount_sat,
{1:ripemd(payment_hash), 4:our_pubkey, 6:locktime, 9:their_pubkey} lightning_amount_sat=lightning_amount_sat,
) payment_hash=payment_hash,
self.wallet.save_invoice(Invoice.from_bech32(invoice)) our_privkey=our_privkey,
prepay_invoice = None their_pubkey=their_pubkey,
prepay_hash = None invoice=None,
else: prepay=True,
onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) )
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):
""" if invoice is None, create a hold invoice """
if prepay:
prepay_amount_sat = self.get_claim_fee() * 2 prepay_amount_sat = self.get_claim_fee() * 2
main_amount_sat = lightning_amount_sat - prepay_amount_sat invoice_amount_sat = lightning_amount_sat - prepay_amount_sat
lnaddr, invoice = self.lnworker.get_bolt11_invoice( else:
invoice_amount_sat = lightning_amount_sat
if not invoice:
_, invoice = self.lnworker.get_bolt11_invoice(
payment_hash=payment_hash, payment_hash=payment_hash,
amount_msat=main_amount_sat * 1000, amount_msat=invoice_amount_sat * 1000,
message='Submarine swap', message='Submarine swap',
expiry=3600 * 24, expiry=3600 * 24,
fallback_address=None, fallback_address=None,
channels=None, channels=None,
) )
# add payment info to lnworker # add payment info to lnworker
self.lnworker.add_payment_info_for_hold_invoice(payment_hash, main_amount_sat) self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat)
self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback) self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback)
if prepay:
prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000) prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000)
_, prepay_invoice = self.lnworker.get_bolt11_invoice( _, prepay_invoice = self.lnworker.get_bolt11_invoice(
payment_hash=prepay_hash, payment_hash=prepay_hash,
amount_msat=prepay_amount_sat * 1000, amount_msat=prepay_amount_sat * 1000,
message='prepay', message='Submarine swap mining fees',
expiry=3600 * 24, expiry=3600 * 24,
fallback_address=None, fallback_address=None,
channels=None, channels=None,
) )
self.lnworker.bundle_payments([payment_hash, prepay_hash]) self.lnworker.bundle_payments([payment_hash, prepay_hash])
self.prepayments[prepay_hash] = payment_hash
else:
prepay_invoice = None
prepay_hash = None
lockup_address = script_to_p2wsh(redeem_script)
receive_address = self.wallet.get_receiving_address()
swap = SwapData(
redeem_script = bytes.fromhex(redeem_script),
locktime = locktime,
privkey = our_privkey,
preimage = None,
prepay_hash = prepay_hash,
lockup_address = lockup_address,
onchain_amount = onchain_amount_sat,
receive_address = receive_address,
lightning_amount = lightning_amount_sat,
is_reverse = False,
is_redeemed = False,
funding_txid = None,
spending_txid = None,
)
swap._payment_hash = payment_hash
self._add_or_reindex_swap(swap)
self.add_lnwatcher_callback(swap)
return swap, invoice, prepay_invoice
def create_reverse_swap(self, *, lightning_amount_sat=None, payment_hash=None, their_pubkey=None):
""" server method. payment_hash is not None for old clients """
locktime = self.network.get_local_height() + 140
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)
#
if payment_hash is None:
preimage = os.urandom(32)
assert lightning_amount_sat is not None
payment_hash = sha256(preimage)
redeem_script = construct_script( redeem_script = construct_script(
WITNESS_TEMPLATE_REVERSE_SWAP, WITNESS_TEMPLATE_REVERSE_SWAP,
{1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey} {1:32, 5:ripemd(payment_hash), 7:our_pubkey, 10:locktime, 13:their_pubkey}
)
else:
# old client
preimage = None
redeem_script = construct_script(
WITNESS_TEMPLATE_SWAP,
{1:ripemd(payment_hash), 4:our_pubkey, 6:locktime, 9:their_pubkey}
) )
swap = self.add_reverse_swap(
redeem_script=redeem_script,
locktime=locktime,
privkey=privkey,
preimage=preimage,
payment_hash=payment_hash,
prepay_hash=None,
onchain_amount_sat=onchain_amount_sat,
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):
lockup_address = script_to_p2wsh(redeem_script) lockup_address = script_to_p2wsh(redeem_script)
receive_address = self.wallet.get_receiving_address() receive_address = self.wallet.get_receiving_address()
swap = SwapData( swap = SwapData(
redeem_script = bytes.fromhex(redeem_script), redeem_script = bytes.fromhex(redeem_script),
locktime = locktime, locktime = locktime,
privkey = privkey, privkey = privkey,
preimage = None, preimage = preimage,
prepay_hash = prepay_hash, prepay_hash = prepay_hash,
lockup_address = lockup_address, lockup_address = lockup_address,
onchain_amount = onchain_amount_sat, onchain_amount = onchain_amount_sat,
receive_address = receive_address, receive_address = receive_address,
lightning_amount = lightning_amount_sat, lightning_amount = lightning_amount_sat,
is_reverse = is_reverse_for_server, is_reverse = True,
is_redeemed = False, is_redeemed = False,
funding_txid = None, funding_txid = None,
spending_txid = None, spending_txid = None,
) )
if prepay_hash:
self.prepayments[prepay_hash] = payment_hash
swap._payment_hash = payment_hash swap._payment_hash = payment_hash
self._add_or_reindex_swap(swap) self._add_or_reindex_swap(swap)
self.add_lnwatcher_callback(swap) self.add_lnwatcher_callback(swap)
return swap, payment_hash, invoice, prepay_invoice return swap
def add_invoice(self, invoice, pay_now=False):
invoice = Invoice.from_bech32(invoice)
key = invoice.rhash
payment_hash = bytes.fromhex(key)
assert key in self.swaps
self.wallet.save_invoice(invoice)
if pay_now:
# check that we have the preimage
swap = self.get_swap(payment_hash)
assert sha256(swap.preimage) == payment_hash
assert swap.spending_txid is None
self.invoices_to_pay.add(key)
async def normal_swap( async def normal_swap(
self, self,
@ -397,54 +516,73 @@ class SwapManager(Logger):
- User creates on-chain output locked to RHASH. - User creates on-chain output locked to RHASH.
- Server pays LN invoice. User reveals preimage. - Server pays LN invoice. User reveals preimage.
- Server spends the on-chain output using 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
""" """
assert self.network assert self.network
assert self.lnwatcher assert self.lnwatcher
privkey = os.urandom(32)
pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
amount_msat = lightning_amount_sat * 1000 amount_msat = lightning_amount_sat * 1000
payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat) refund_privkey = os.urandom(32)
lnaddr, invoice = self.lnworker.get_bolt11_invoice( refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True)
payment_hash=payment_hash,
amount_msat=amount_msat, if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST:
message='swap', self.logger.info('requesting preimage hash for swap')
expiry=3600 * 24, request_data = {
fallback_address=None, "invoiceAmount": lightning_amount_sat,
channels=channels, "refundPublicKey": refund_pubkey.hex()
) }
preimage = self.lnworker.get_preimage(payment_hash) response = await self.network.async_send_http_on_proxy(
request_data = { 'post',
"type": "submarine", self.api_url + '/createnormalswap',
"pairId": "BTC/BTC", json=request_data,
"orderSide": "sell", timeout=30)
"invoice": invoice, data = json.loads(response)
"refundPublicKey": pubkey.hex() payment_hash = bytes.fromhex(data["preimageHash"])
} preimage = None
response = await self.network.async_send_http_on_proxy( invoice = None
'post', else:
self.api_url + '/createswap', # create invoice, send it to server
json=request_data, payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat)
timeout=30) preimage = self.lnworker.get_preimage(payment_hash)
data = json.loads(response) _, invoice = self.lnworker.get_bolt11_invoice(
response_id = data["id"] payment_hash=payment_hash,
amount_msat=amount_msat,
message='swap',
expiry=3600 * 24,
fallback_address=None,
channels=channels,
)
request_data = {
"type": "submarine",
"pairId": "BTC/BTC",
"orderSide": "sell",
"invoice": invoice,
"refundPublicKey": refund_pubkey.hex()
}
response = await self.network.async_send_http_on_proxy(
'post',
self.api_url + '/createswap',
json=request_data,
timeout=30)
data = json.loads(response)
response_id = data["id"]
zeroconf = data["acceptZeroConf"] zeroconf = data["acceptZeroConf"]
onchain_amount = data["expectedAmount"] onchain_amount = data["expectedAmount"]
locktime = data["timeoutBlockHeight"] locktime = data["timeoutBlockHeight"]
lockup_address = data["address"] lockup_address = data["address"]
redeem_script = data["redeemScript"] redeem_script = data["redeemScript"]
# verify redeem_script is built with our pubkey and preimage # verify redeem_script is built with our pubkey and preimage
redeem_script = bytes.fromhex(redeem_script) if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST:
parsed_script = [x for x in script_GetOp(redeem_script)] claim_pubkey, _ = check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey)
if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_SWAP): else:
raise Exception("fswap check failed: scriptcode does not match template") claim_pubkey, _ = check_normal_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey)
if script_to_p2wsh(redeem_script.hex()) != lockup_address:
raise Exception("fswap check failed: inconsistent scriptcode and address")
if hash_160(preimage) != parsed_script[1][1]:
raise Exception("fswap check failed: our preimage not in script")
if pubkey != parsed_script[9][1]:
raise Exception("fswap check failed: our pubkey not in script")
if locktime != int.from_bytes(parsed_script[6][1], byteorder='little'):
raise Exception("fswap check failed: inconsistent locktime and script")
# check that onchain_amount is not more than what we estimated # check that onchain_amount is not more than what we estimated
if onchain_amount > expected_onchain_amount_sat: if onchain_amount > expected_onchain_amount_sat:
raise Exception(f"fswap check failed: onchain_amount is more than what we estimated: " raise Exception(f"fswap check failed: onchain_amount is more than what we estimated: "
@ -452,30 +590,41 @@ class SwapManager(Logger):
# verify that they are not locking up funds for more than a day # verify that they are not locking up funds for more than a day
if locktime - self.network.get_local_height() >= 144: if locktime - self.network.get_local_height() >= 144:
raise Exception("fswap check failed: locktime too far in future") raise Exception("fswap check failed: locktime too far in future")
# save swap data in wallet in case we need a refund
receive_address = self.wallet.get_receiving_address() swap, invoice, _ = self.add_normal_swap(
swap = SwapData( redeem_script=redeem_script,
redeem_script = redeem_script, locktime=locktime,
locktime = locktime, lightning_amount_sat=lightning_amount_sat,
privkey = privkey, onchain_amount_sat=onchain_amount,
preimage = preimage, payment_hash=payment_hash,
prepay_hash = None, our_privkey=refund_privkey,
lockup_address = lockup_address, their_pubkey=claim_pubkey,
onchain_amount = onchain_amount, invoice=invoice,
receive_address = receive_address, prepay=False)
lightning_amount = lightning_amount_sat,
is_reverse = False, if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST:
is_redeemed = False, # send invoice to server and wait for htlcs
funding_txid = None, request_data = {
spending_txid = None, "preimageHash": payment_hash.hex(),
) "invoice": invoice,
swap._payment_hash = payment_hash "refundPublicKey": refund_pubkey.hex(),
self._add_or_reindex_swap(swap) }
self.add_lnwatcher_callback(swap) response = await self.network.async_send_http_on_proxy(
return await self.start_normal_swap(swap, tx, password) 'post',
self.api_url + '/addswapinvoice',
json=request_data,
timeout=30)
data = json.loads(response)
# wait for funding tx
while swap.funding_txid is None:
await asyncio.sleep(0.1)
else:
# broadcast funding tx right away
await self.broadcast_funding_tx(swap, tx, password)
return swap.funding_txid
@log_exceptions @log_exceptions
async def start_normal_swap(self, swap, tx, password): async def broadcast_funding_tx(self, swap, tx, password):
# create funding tx # create funding tx
# note: rbf must not decrease payment # note: rbf must not decrease payment
# this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output
@ -488,9 +637,9 @@ class SwapManager(Logger):
tx.add_outputs([funding_output]) tx.add_outputs([funding_output])
tx.set_rbf(True) tx.set_rbf(True)
self.wallet.sign_transaction(tx, password) self.wallet.sign_transaction(tx, password)
await self.network.broadcast_transaction(tx) await self.network.broadcast_transaction(tx)
swap.funding_txid = tx.txid() swap.funding_txid = tx.txid()
return swap.funding_txid
async def reverse_swap( async def reverse_swap(
self, self,
@ -513,16 +662,16 @@ class SwapManager(Logger):
assert self.network assert self.network
assert self.lnwatcher assert self.lnwatcher
privkey = os.urandom(32) privkey = os.urandom(32)
pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
preimage = os.urandom(32) preimage = os.urandom(32)
preimage_hash = sha256(preimage) payment_hash = sha256(preimage)
request_data = { request_data = {
"type": "reversesubmarine", "type": "reversesubmarine",
"pairId": "BTC/BTC", "pairId": "BTC/BTC",
"orderSide": "buy", "orderSide": "buy",
"invoiceAmount": lightning_amount_sat, "invoiceAmount": lightning_amount_sat,
"preimageHash": preimage_hash.hex(), "preimageHash": payment_hash.hex(),
"claimPublicKey": pubkey.hex() "claimPublicKey": our_pubkey.hex()
} }
response = await self.network.async_send_http_on_proxy( response = await self.network.async_send_http_on_proxy(
'post', 'post',
@ -538,18 +687,7 @@ class SwapManager(Logger):
onchain_amount = data["onchainAmount"] onchain_amount = data["onchainAmount"]
response_id = data['id'] response_id = data['id']
# verify redeem_script is built with our pubkey and preimage # verify redeem_script is built with our pubkey and preimage
redeem_script = bytes.fromhex(redeem_script) check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=None, claim_pubkey=our_pubkey)
parsed_script = [x for x in script_GetOp(redeem_script)]
if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP):
raise Exception("rswap check failed: scriptcode does not match template")
if script_to_p2wsh(redeem_script.hex()) != lockup_address:
raise Exception("rswap check failed: inconsistent scriptcode and address")
if hash_160(preimage) != parsed_script[5][1]:
raise Exception("rswap check failed: our preimage not in script")
if pubkey != parsed_script[7][1]:
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")
# check that the onchain amount is what we expected # check that the onchain amount is what we expected
if onchain_amount < expected_onchain_amount_sat: if onchain_amount < expected_onchain_amount_sat:
raise Exception(f"rswap check failed: onchain_amount is less than what we expected: " raise Exception(f"rswap check failed: onchain_amount is less than what we expected: "
@ -557,10 +695,10 @@ class SwapManager(Logger):
# verify that we will have enough time to get our tx confirmed # verify that we will have enough time to get our tx confirmed
if locktime - self.network.get_local_height() <= MIN_LOCKTIME_DELTA: if locktime - self.network.get_local_height() <= MIN_LOCKTIME_DELTA:
raise Exception("rswap check failed: locktime too close") raise Exception("rswap check failed: locktime too close")
# verify invoice preimage_hash # verify invoice payment_hash
lnaddr = self.lnworker._check_invoice(invoice) lnaddr = self.lnworker._check_invoice(invoice)
invoice_amount = int(lnaddr.get_amount_sat()) invoice_amount = int(lnaddr.get_amount_sat())
if lnaddr.paymenthash != preimage_hash: if lnaddr.paymenthash != payment_hash:
raise Exception("rswap check failed: inconsistent RHASH and invoice") raise Exception("rswap check failed: inconsistent RHASH and invoice")
# check that the lightning amount is what we requested # check that the lightning amount is what we requested
if fee_invoice: if fee_invoice:
@ -573,29 +711,17 @@ class SwapManager(Logger):
raise Exception(f"rswap check failed: invoice_amount ({invoice_amount}) " raise Exception(f"rswap check failed: invoice_amount ({invoice_amount}) "
f"not what we requested ({lightning_amount_sat})") f"not what we requested ({lightning_amount_sat})")
# save swap data to wallet file # save swap data to wallet file
receive_address = self.wallet.get_receiving_address() swap = self.add_reverse_swap(
swap = SwapData( redeem_script=redeem_script,
redeem_script = redeem_script, locktime=locktime,
locktime = locktime, privkey=privkey,
privkey = privkey, preimage=preimage,
preimage = preimage, payment_hash=payment_hash,
prepay_hash = prepay_hash, prepay_hash=prepay_hash,
lockup_address = lockup_address, onchain_amount_sat=onchain_amount,
onchain_amount = onchain_amount, lightning_amount_sat=lightning_amount_sat)
receive_address = receive_address,
lightning_amount = lightning_amount_sat,
is_reverse = True,
is_redeemed = False,
funding_txid = None,
spending_txid = None,
)
swap._payment_hash = preimage_hash
self._add_or_reindex_swap(swap)
# add callback to lnwatcher
self.add_lnwatcher_callback(swap)
# initiate fee payment. # initiate fee payment.
if fee_invoice: if fee_invoice:
self.prepayments[prepay_hash] = preimage_hash
asyncio.ensure_future(self.lnworker.pay_invoice(fee_invoice, attempts=10)) asyncio.ensure_future(self.lnworker.pay_invoice(fee_invoice, attempts=10))
# we return if we detect funding # we return if we detect funding
async def wait_for_funding(swap): async def wait_for_funding(swap):

Loading…
Cancel
Save