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'])
assert len(their_pubkey) == 33
swap = self.sm.create_reverse_swap(
payment_hash=None,
lightning_amount_sat=lightning_amount_sat,
their_pubkey=their_pubkey
)
@ -121,6 +120,8 @@ class SwapServer(Logger, EventListener):
return web.json_response(response)
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()
request = await r.json()
req_type = request['type']
@ -145,28 +146,6 @@ class SwapServer(Logger, EventListener):
'timeoutBlockHeight': swap.locktime,
"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:
raise Exception('unsupported request type:' + req_type)
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
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
# 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")
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):
def __str__(self):
@ -188,7 +156,6 @@ class SwapManager(Logger):
self.percentage = 0
self._min_amount = None
self._max_amount = None
self.server_supports_htlc_first = False
self.wallet = wallet
self.lnworker = lnworker
self.taskgroup = None
@ -507,28 +474,19 @@ class SwapManager(Logger):
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 """
def create_reverse_swap(self, *, lightning_amount_sat=None, their_pubkey=None):
""" server method. """
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)
#
if payment_hash is None:
preimage = os.urandom(32)
assert lightning_amount_sat is not None
payment_hash = sha256(preimage)
redeem_script = construct_script(
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}
)
preimage = os.urandom(32)
assert lightning_amount_sat is not None
payment_hash = sha256(preimage)
redeem_script = construct_script(
WITNESS_TEMPLATE_REVERSE_SWAP,
{1:32, 5:ripemd(payment_hash), 7:our_pubkey, 10:locktime, 13:their_pubkey}
)
swap = self.add_reverse_swap(
redeem_script=redeem_script,
locktime=locktime,
@ -612,48 +570,20 @@ class SwapManager(Logger):
refund_privkey = os.urandom(32)
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')
request_data = {
"invoiceAmount": lightning_amount_sat,
"refundPublicKey": refund_pubkey.hex()
}
response = await self.network.async_send_http_on_proxy(
'post',
self.api_url + '/createnormalswap',
json=request_data,
timeout=30)
data = json.loads(response)
payment_hash = bytes.fromhex(data["preimageHash"])
preimage = 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"]
self.logger.info('requesting preimage hash for swap')
request_data = {
"invoiceAmount": lightning_amount_sat,
"refundPublicKey": refund_pubkey.hex()
}
response = await self.network.async_send_http_on_proxy(
'post',
self.api_url + '/createnormalswap',
json=request_data,
timeout=30)
data = json.loads(response)
payment_hash = bytes.fromhex(data["preimageHash"])
preimage = None
invoice = None
zeroconf = data["acceptZeroConf"]
onchain_amount = data["expectedAmount"]
@ -661,10 +591,7 @@ class SwapManager(Logger):
lockup_address = data["address"]
redeem_script = data["redeemScript"]
# 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)
else:
claim_pubkey, _ = check_normal_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)
# check that onchain_amount is not more than what we estimated
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):
payment_hash = swap.payment_hash
refund_pubkey = ECPrivkey(swap.privkey).get_public_key_bytes(compressed=True)
if self.server_supports_htlc_first:
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
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)
return swap.funding_txid
def create_funding_tx(self, swap, tx, password, *, batch_rbf: Optional[bool] = None):
@ -743,7 +666,6 @@ class SwapManager(Logger):
else:
return
await self.get_pairs()
assert self.server_supports_htlc_first
lightning_amount_sat = self.get_recv_amount(change_amount, is_reverse=False)
swap, invoice = await self.request_normal_swap(
lightning_amount_sat = lightning_amount_sat,
@ -887,7 +809,7 @@ class SwapManager(Logger):
limits = pairs['pairs']['BTC/BTC']['limits']
self._min_amount = limits['minimal']
self._max_amount = limits['maximal']
self.server_supports_htlc_first = pairs.get('htlcFirst', False)
assert pairs.get('htlcFirst') is True
def pairs_filename(self):
return os.path.join(self.wallet.config.path, 'swap_pairs')

Loading…
Cancel
Save