Browse Source

submarine swaps: remove support for 'old' normal swaps,

where the user has the preimage.

The CLTV requirements between old and new flow are imcompatible.
With the current locktime value, the server was vulnerable to an
attack where the client does not settle the lightning payment
and claims a refund. In order to support both old and new flows,
one would need to use different locktimes.
master
ThomasV 2 years ago
parent
commit
fb4eb86e7c
  1. 25
      electrum/plugins/swapserver/server.py
  2. 168
      electrum/submarine_swaps.py

25
electrum/plugins/swapserver/server.py

@ -105,7 +105,6 @@ class SwapServer(Logger, EventListener):
their_pubkey = bytes.fromhex(request['refundPublicKey']) their_pubkey = bytes.fromhex(request['refundPublicKey'])
assert len(their_pubkey) == 33 assert len(their_pubkey) == 33
swap = self.sm.create_reverse_swap( swap = self.sm.create_reverse_swap(
payment_hash=None,
lightning_amount_sat=lightning_amount_sat, lightning_amount_sat=lightning_amount_sat,
their_pubkey=their_pubkey their_pubkey=their_pubkey
) )
@ -121,6 +120,8 @@ class SwapServer(Logger, EventListener):
return web.json_response(response) return web.json_response(response)
async def create_swap(self, r): async def create_swap(self, r):
# reverse for client, forward for server
# requesting a normal swap (old protocol) will raise an exception
self.sm.init_pairs() self.sm.init_pairs()
request = await r.json() request = await r.json()
req_type = request['type'] req_type = request['type']
@ -145,28 +146,6 @@ class SwapServer(Logger, EventListener):
'timeoutBlockHeight': swap.locktime, 'timeoutBlockHeight': swap.locktime,
"onchainAmount": swap.onchain_amount, "onchainAmount": swap.onchain_amount,
} }
elif req_type == 'submarine':
# old protocol
their_invoice=request['invoice']
their_pubkey=bytes.fromhex(request['refundPublicKey'])
assert len(their_pubkey) == 33
lnaddr = lndecode(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
)
self.sm.add_invoice(their_invoice, pay_now=False)
response = {
"id": payment_hash.hex(),
"acceptZeroConf": False,
"expectedAmount": swap.onchain_amount,
"timeoutBlockHeight": swap.locktime,
"address": swap.lockup_address,
"redeemScript": swap.redeem_script.hex()
}
else: else:
raise Exception('unsupported request type:' + req_type) raise Exception('unsupported request type:' + req_type)
return web.json_response(response) return web.json_response(response)

168
electrum/submarine_swaps.py

@ -47,21 +47,6 @@ LOCKUP_FEE_SIZE = 153 # assuming 1 output, 2 outputs
MIN_LOCKTIME_DELTA = 60 MIN_LOCKTIME_DELTA = 60
LOCKTIME_DELTA_REFUND = 70 LOCKTIME_DELTA_REFUND = 70
WITNESS_TEMPLATE_SWAP = [
opcodes.OP_HASH160,
OPPushDataGeneric(lambda x: x == 20),
opcodes.OP_EQUAL,
opcodes.OP_IF,
OPPushDataPubkey,
opcodes.OP_ELSE,
OPPushDataGeneric(None),
opcodes.OP_CHECKLOCKTIMEVERIFY,
opcodes.OP_DROP,
OPPushDataPubkey,
opcodes.OP_ENDIF,
opcodes.OP_CHECKSIG
]
# The script of the reverse swaps has one extra check in it to verify # The script of the reverse swaps has one extra check in it to verify
# that the length of the preimage is 32. This is required because in # that the length of the preimage is 32. This is required because in
@ -107,23 +92,6 @@ def check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, loc
raise Exception("rswap check failed: inconsistent locktime and script") raise Exception("rswap check failed: inconsistent locktime and script")
return parsed_script[7][1], parsed_script[13][1] 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):
@ -188,7 +156,6 @@ class SwapManager(Logger):
self.percentage = 0 self.percentage = 0
self._min_amount = None self._min_amount = None
self._max_amount = None self._max_amount = None
self.server_supports_htlc_first = False
self.wallet = wallet self.wallet = wallet
self.lnworker = lnworker self.lnworker = lnworker
self.taskgroup = None self.taskgroup = None
@ -507,28 +474,19 @@ class SwapManager(Logger):
self.add_lnwatcher_callback(swap) self.add_lnwatcher_callback(swap)
return swap, invoice, prepay_invoice return swap, invoice, prepay_invoice
def create_reverse_swap(self, *, lightning_amount_sat=None, payment_hash=None, their_pubkey=None): def create_reverse_swap(self, *, lightning_amount_sat=None, their_pubkey=None):
""" server method. payment_hash is not None for old clients """ """ server method. """
locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND
privkey = os.urandom(32) privkey = os.urandom(32)
our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False) onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False)
# preimage = os.urandom(32)
if payment_hash is None: assert lightning_amount_sat is not None
preimage = os.urandom(32) payment_hash = sha256(preimage)
assert lightning_amount_sat is not None redeem_script = construct_script(
payment_hash = sha256(preimage) WITNESS_TEMPLATE_REVERSE_SWAP,
redeem_script = construct_script( {1:32, 5:ripemd(payment_hash), 7:our_pubkey, 10:locktime, 13:their_pubkey}
WITNESS_TEMPLATE_REVERSE_SWAP, )
{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( swap = self.add_reverse_swap(
redeem_script=redeem_script, redeem_script=redeem_script,
locktime=locktime, locktime=locktime,
@ -612,48 +570,20 @@ class SwapManager(Logger):
refund_privkey = os.urandom(32) refund_privkey = os.urandom(32)
refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True) refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True)
if self.server_supports_htlc_first: self.logger.info('requesting preimage hash for swap')
self.logger.info('requesting preimage hash for swap') request_data = {
request_data = { "invoiceAmount": lightning_amount_sat,
"invoiceAmount": lightning_amount_sat, "refundPublicKey": refund_pubkey.hex()
"refundPublicKey": refund_pubkey.hex() }
} response = await self.network.async_send_http_on_proxy(
response = await self.network.async_send_http_on_proxy( 'post',
'post', self.api_url + '/createnormalswap',
self.api_url + '/createnormalswap', json=request_data,
json=request_data, timeout=30)
timeout=30) data = json.loads(response)
data = json.loads(response) payment_hash = bytes.fromhex(data["preimageHash"])
payment_hash = bytes.fromhex(data["preimageHash"]) preimage = None
preimage = None invoice = None
invoice = None
else:
# create invoice, send it to server
payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat)
preimage = self.lnworker.get_preimage(payment_hash)
_, invoice = self.lnworker.get_bolt11_invoice(
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"]
@ -661,10 +591,7 @@ class SwapManager(Logger):
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
if self.server_supports_htlc_first: claim_pubkey, _ = check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey)
claim_pubkey, _ = check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey)
else:
claim_pubkey, _ = check_normal_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey)
# 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:
@ -689,31 +616,27 @@ class SwapManager(Logger):
async def wait_for_htlcs_and_broadcast(self, swap, invoice, tx, channels=None): async def wait_for_htlcs_and_broadcast(self, swap, invoice, tx, channels=None):
payment_hash = swap.payment_hash payment_hash = swap.payment_hash
refund_pubkey = ECPrivkey(swap.privkey).get_public_key_bytes(compressed=True) refund_pubkey = ECPrivkey(swap.privkey).get_public_key_bytes(compressed=True)
if self.server_supports_htlc_first: async def callback(payment_hash):
async def callback(payment_hash):
await self.broadcast_funding_tx(swap, tx)
self.lnworker.register_hold_invoice(payment_hash, callback)
# send invoice to server and wait for htlcs
request_data = {
"preimageHash": payment_hash.hex(),
"invoice": invoice,
"refundPublicKey": refund_pubkey.hex(),
}
response = await self.network.async_send_http_on_proxy(
'post',
self.api_url + '/addswapinvoice',
json=request_data,
timeout=30)
data = json.loads(response)
# wait for funding tx
lnaddr = lndecode(invoice)
while swap.funding_txid is None and not lnaddr.is_expired():
await asyncio.sleep(0.1)
else:
# broadcast funding tx right away
await self.broadcast_funding_tx(swap, tx) await self.broadcast_funding_tx(swap, tx)
self.lnworker.register_hold_invoice(payment_hash, callback)
# send invoice to server and wait for htlcs
request_data = {
"preimageHash": payment_hash.hex(),
"invoice": invoice,
"refundPublicKey": refund_pubkey.hex(),
}
response = await self.network.async_send_http_on_proxy(
'post',
self.api_url + '/addswapinvoice',
json=request_data,
timeout=30)
data = json.loads(response)
# wait for funding tx
lnaddr = lndecode(invoice)
while swap.funding_txid is None and not lnaddr.is_expired():
await asyncio.sleep(0.1)
return swap.funding_txid return swap.funding_txid
def create_funding_tx(self, swap, tx, password, *, batch_rbf: Optional[bool] = None): def create_funding_tx(self, swap, tx, password, *, batch_rbf: Optional[bool] = None):
@ -743,7 +666,6 @@ class SwapManager(Logger):
else: else:
return return
await self.get_pairs() await self.get_pairs()
assert self.server_supports_htlc_first
lightning_amount_sat = self.get_recv_amount(change_amount, is_reverse=False) lightning_amount_sat = self.get_recv_amount(change_amount, is_reverse=False)
swap, invoice = await self.request_normal_swap( swap, invoice = await self.request_normal_swap(
lightning_amount_sat = lightning_amount_sat, lightning_amount_sat = lightning_amount_sat,
@ -887,7 +809,7 @@ class SwapManager(Logger):
limits = pairs['pairs']['BTC/BTC']['limits'] limits = pairs['pairs']['BTC/BTC']['limits']
self._min_amount = limits['minimal'] self._min_amount = limits['minimal']
self._max_amount = limits['maximal'] self._max_amount = limits['maximal']
self.server_supports_htlc_first = pairs.get('htlcFirst', False) assert pairs.get('htlcFirst') is True
def pairs_filename(self): def pairs_filename(self):
return os.path.join(self.wallet.config.path, 'swap_pairs') return os.path.join(self.wallet.config.path, 'swap_pairs')

Loading…
Cancel
Save