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.
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user