If we have proposed htlcs in a channel that was force-closed,
call lnworker.htlc_failed once the htlc_tx is deeply mined. In the case of a forwarding, this will fail incoming htlcs. (fixes #8547)
This commit is contained in:
@@ -321,7 +321,7 @@ class AbstractChannel(Logger, ABC):
|
||||
def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, SweepInfo]:
|
||||
return {}
|
||||
|
||||
def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None:
|
||||
def extract_preimage_from_htlc_txin(self, txin: TxInput, *, is_deeply_mined: bool) -> None:
|
||||
return
|
||||
|
||||
def update_onchain_state(self, *, funding_txid: str, funding_height: TxMinedInfo,
|
||||
@@ -658,7 +658,7 @@ class ChannelBackup(AbstractChannel):
|
||||
def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, SweepInfo]:
|
||||
return {}
|
||||
|
||||
def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None:
|
||||
def extract_preimage_from_htlc_txin(self, txin: TxInput, *, is_deeply_mined: bool) -> None:
|
||||
return None
|
||||
|
||||
def get_funding_address(self):
|
||||
@@ -1350,42 +1350,72 @@ class Channel(AbstractChannel):
|
||||
error_bytes, failure_message = None, None
|
||||
self.lnworker.htlc_failed(self, htlc.payment_hash, htlc.htlc_id, error_bytes, failure_message)
|
||||
|
||||
def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None:
|
||||
|
||||
def extract_preimage_from_htlc_txin(self, txin: TxInput, *, is_deeply_mined: bool) -> None:
|
||||
from . import lnutil
|
||||
from .crypto import ripemd
|
||||
from .transaction import match_script_against_template, script_GetOp
|
||||
from .lnonion import OnionRoutingFailure, OnionFailureCode
|
||||
witness = txin.witness_elements()
|
||||
if len(witness) == 5: # HTLC success tx
|
||||
preimage = witness[3]
|
||||
elif len(witness) == 3: # spending offered HTLC directly from ctx
|
||||
preimage = witness[1]
|
||||
witness_script = witness[-1]
|
||||
script_ops = [x for x in script_GetOp(witness_script)]
|
||||
if match_script_against_template(witness_script, lnutil.WITNESS_TEMPLATE_OFFERED_HTLC, debug=False) \
|
||||
or match_script_against_template(witness_script, lnutil.WITNESS_TEMPLATE_OFFERED_HTLC_ANCHORS, debug=False):
|
||||
ripemd_payment_hash = script_ops[21][1]
|
||||
elif match_script_against_template(witness_script, lnutil.WITNESS_TEMPLATE_RECEIVED_HTLC, debug=False) \
|
||||
or match_script_against_template(witness_script, lnutil.WITNESS_TEMPLATE_RECEIVED_HTLC_ANCHORS, debug=False):
|
||||
ripemd_payment_hash = script_ops[14][1]
|
||||
else:
|
||||
return
|
||||
payment_hash = sha256(preimage)
|
||||
found = {}
|
||||
for direction, htlc in itertools.chain(
|
||||
self.hm.get_htlcs_in_oldest_unrevoked_ctx(REMOTE),
|
||||
self.hm.get_htlcs_in_latest_ctx(REMOTE)):
|
||||
if htlc.payment_hash == payment_hash:
|
||||
if ripemd(htlc.payment_hash) == ripemd_payment_hash:
|
||||
is_sent = direction == RECEIVED
|
||||
found[htlc.htlc_id] = (htlc, is_sent)
|
||||
for direction, htlc in itertools.chain(
|
||||
self.hm.get_htlcs_in_oldest_unrevoked_ctx(LOCAL),
|
||||
self.hm.get_htlcs_in_latest_ctx(LOCAL)):
|
||||
if htlc.payment_hash == payment_hash:
|
||||
if ripemd(htlc.payment_hash) == ripemd_payment_hash:
|
||||
is_sent = direction == SENT
|
||||
found[htlc.htlc_id] = (htlc, is_sent)
|
||||
if not found:
|
||||
return
|
||||
if self.lnworker.get_preimage(payment_hash) is not None:
|
||||
return
|
||||
self.logger.info(f"found preimage in witness of length {len(witness)}, for {payment_hash.hex()}")
|
||||
# ^ note: log message text grepped for in regtests
|
||||
self.lnworker.save_preimage(payment_hash, preimage)
|
||||
for htlc, is_sent in found.values():
|
||||
if is_sent:
|
||||
self.lnworker.htlc_fulfilled(self, payment_hash, htlc.htlc_id)
|
||||
else:
|
||||
# FIXME
|
||||
#self.lnworker.htlc_received(self, payment_hash)
|
||||
pass
|
||||
if len(witness) == 5: # HTLC success tx
|
||||
preimage = witness[3]
|
||||
elif len(witness) == 3: # spending offered HTLC directly from ctx
|
||||
preimage = witness[1]
|
||||
else:
|
||||
preimage = None # HTLC timeout tx
|
||||
if preimage:
|
||||
assert ripemd(sha256(preimage)) == ripemd_payment_hash
|
||||
payment_hash = sha256(preimage)
|
||||
if self.lnworker.get_preimage(payment_hash) is not None:
|
||||
return
|
||||
# ^ note: log message text grepped for in regtests
|
||||
self.logger.info(f"found preimage in witness of length {len(witness)}, for {payment_hash.hex()}")
|
||||
|
||||
if preimage:
|
||||
self.lnworker.save_preimage(payment_hash, preimage)
|
||||
for htlc, is_sent in found.values():
|
||||
if is_sent:
|
||||
self.lnworker.htlc_fulfilled(self, payment_hash, htlc.htlc_id)
|
||||
else:
|
||||
# htlc timeout tx
|
||||
if not is_deeply_mined:
|
||||
return
|
||||
failure = OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
|
||||
for htlc, is_sent in found.values():
|
||||
if is_sent:
|
||||
self.logger.info(f'htlc timeout tx: failing htlc {is_sent}')
|
||||
self.lnworker.htlc_failed(
|
||||
self,
|
||||
payment_hash=htlc.payment_hash,
|
||||
htlc_id=htlc.htlc_id,
|
||||
error_bytes=None,
|
||||
failure_message=failure)
|
||||
|
||||
|
||||
def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int:
|
||||
assert type(whose) is HTLCOwner
|
||||
|
||||
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
|
||||
from .network import Network
|
||||
from .lnsweep import SweepInfo
|
||||
from .lnworker import LNWallet
|
||||
|
||||
from .lnchannel import AbstractChannel
|
||||
|
||||
class TxMinedDepth(IntEnum):
|
||||
""" IntEnum because we call min() in get_deepest_tx_mined_depth_for_txids """
|
||||
@@ -156,7 +156,7 @@ class LNWatcher(Logger, EventListener):
|
||||
return TxMinedDepth.FREE
|
||||
tx_mined_depth = self.adb.get_tx_height(txid)
|
||||
height, conf = tx_mined_depth.height, tx_mined_depth.conf
|
||||
if conf > 100:
|
||||
if conf > 20:
|
||||
return TxMinedDepth.DEEP
|
||||
elif conf > 0:
|
||||
return TxMinedDepth.SHALLOW
|
||||
@@ -175,7 +175,6 @@ class LNWatcher(Logger, EventListener):
|
||||
|
||||
|
||||
|
||||
|
||||
class LNWalletWatcher(LNWatcher):
|
||||
|
||||
def __init__(self, lnworker: 'LNWallet', network: 'Network'):
|
||||
@@ -263,12 +262,8 @@ class LNWalletWatcher(LNWatcher):
|
||||
else:
|
||||
keep_watching = True
|
||||
await self.maybe_redeem(prevout2, htlc_sweep_info, name)
|
||||
# extract preimage
|
||||
keep_watching |= not self.is_deeply_mined(spender_txid)
|
||||
txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout))
|
||||
assert txin_idx is not None
|
||||
spender_txin = spender_tx.inputs()[txin_idx]
|
||||
chan.extract_preimage_from_htlc_txin(spender_txin)
|
||||
self.maybe_extract_preimage(chan, spender_tx, prevout)
|
||||
else:
|
||||
keep_watching = True
|
||||
# broadcast or maybe update our own tx
|
||||
@@ -374,3 +369,12 @@ class LNWalletWatcher(LNWatcher):
|
||||
if old_tx and old_tx.txid() != new_tx.txid():
|
||||
self.lnworker.wallet.set_label(old_tx.txid(), None)
|
||||
util.trigger_callback('wallet_updated', self.lnworker.wallet)
|
||||
|
||||
def maybe_extract_preimage(self, chan: 'AbstractChannel', spender_tx: Transaction, prevout: str):
|
||||
txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout))
|
||||
assert txin_idx is not None
|
||||
spender_txin = spender_tx.inputs()[txin_idx]
|
||||
chan.extract_preimage_from_htlc_txin(
|
||||
spender_txin,
|
||||
is_deeply_mined=self.is_deeply_mined(spender_tx.txid()),
|
||||
)
|
||||
|
||||
@@ -2484,7 +2484,8 @@ class LNWallet(LNWorker):
|
||||
fw_htlcs = self.active_forwardings[fw_key]
|
||||
fw_htlcs.remove(htlc_key)
|
||||
|
||||
if shi := self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id)):
|
||||
shi = self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id))
|
||||
if shi and htlc_id in chan.onion_keys:
|
||||
chan.pop_onion_key(htlc_id)
|
||||
payment_key = payment_hash + shi.payment_secret_orig
|
||||
paysession = self._paysessions[payment_key]
|
||||
@@ -2520,6 +2521,7 @@ class LNWallet(LNWorker):
|
||||
htlc_id: int,
|
||||
error_bytes: Optional[bytes],
|
||||
failure_message: Optional['OnionRoutingFailure']):
|
||||
# note: this may be called several times for the same htlc
|
||||
|
||||
util.trigger_callback('htlc_failed', payment_hash, chan, htlc_id)
|
||||
htlc_key = serialize_htlc_key(chan.get_scid_or_local_alias(), htlc_id)
|
||||
@@ -2528,7 +2530,8 @@ class LNWallet(LNWorker):
|
||||
fw_htlcs = self.active_forwardings[fw_key]
|
||||
fw_htlcs.remove(htlc_key)
|
||||
|
||||
if shi := self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id)):
|
||||
shi = self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id))
|
||||
if shi and htlc_id in chan.onion_keys:
|
||||
onion_key = chan.pop_onion_key(htlc_id)
|
||||
payment_okey = payment_hash + shi.payment_secret_orig
|
||||
paysession = self._paysessions[payment_okey]
|
||||
@@ -2583,10 +2586,11 @@ class LNWallet(LNWorker):
|
||||
fw_htlcs = self.active_forwardings[fw_key]
|
||||
can_forward_failure = (len(fw_htlcs) == 0) and not paysession_active
|
||||
if can_forward_failure:
|
||||
self.logger.info(f'htlc_failed: save_forwarding_failure (phash={payment_hash.hex()})')
|
||||
self.save_forwarding_failure(fw_key, error_bytes=error_bytes, failure_message=failure_message)
|
||||
self.notify_upstream_peer(htlc_key)
|
||||
else:
|
||||
self.logger.info(f"waiting for other htlcs to fail (phash={payment_hash.hex()})")
|
||||
self.logger.info(f'htlc_failed: waiting for other htlcs to fail (phash={payment_hash.hex()})')
|
||||
|
||||
def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=None, needs_jit=False):
|
||||
"""calculate routing hints (BOLT-11 'r' field)"""
|
||||
|
||||
@@ -124,6 +124,22 @@ class TestLightningWatchtower(TestLightning):
|
||||
self.run_shell(['watchtower'])
|
||||
|
||||
|
||||
class TestLightningABC(TestLightning):
|
||||
agents = {
|
||||
'alice': {
|
||||
},
|
||||
'bob': {
|
||||
'lightning_listen': 'localhost:9735',
|
||||
'lightning_forward_payments': 'true',
|
||||
},
|
||||
'carol': {
|
||||
}
|
||||
}
|
||||
|
||||
def test_fw_fail_htlc(self):
|
||||
self.run_shell(['fw_fail_htlc'])
|
||||
|
||||
|
||||
class TestLightningJIT(TestLightning):
|
||||
agents = {
|
||||
'alice': {
|
||||
|
||||
@@ -14,6 +14,7 @@ bitcoin_cli="bitcoin-cli -rpcuser=doggman -rpcpassword=donkey -rpcport=18554 -re
|
||||
|
||||
function new_blocks()
|
||||
{
|
||||
printf "mining $1 blocks\n"
|
||||
$bitcoin_cli generatetoaddress $1 $($bitcoin_cli getnewaddress) > /dev/null
|
||||
}
|
||||
|
||||
@@ -510,6 +511,42 @@ if [[ $1 == "watchtower" ]]; then
|
||||
wait_until_spent $ctx_id $output_index # alice's to_local gets punished
|
||||
fi
|
||||
|
||||
if [[ $1 == "fw_fail_htlc" ]]; then
|
||||
$carol enable_htlc_settle false
|
||||
bob_node=$($bob nodeid)
|
||||
wait_for_balance carol 1
|
||||
echo "alice and carol open channels with bob"
|
||||
chan_id1=$($alice open_channel $bob_node 0.15 --password='' --push_amount=0.075)
|
||||
chan_id2=$($carol open_channel $bob_node 0.15 --password='' --push_amount=0.075)
|
||||
new_blocks 3
|
||||
wait_until_channel_open alice
|
||||
wait_until_channel_open carol
|
||||
echo "alice pays carol"
|
||||
invoice=$($carol add_request 0.01 --lightning -m "invoice" | jq -r ".lightning_invoice")
|
||||
screen -S alice_payment -dm -L -Logfile /tmp/alice/screen1.log $alice lnpay $invoice --timeout=600
|
||||
sleep 1
|
||||
unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')
|
||||
if [[ "$unsettled" == "0" ]]; then
|
||||
echo 'enable_htlc_settle did not work (carol settled)'
|
||||
exit 1
|
||||
fi
|
||||
$carol stop
|
||||
$bob close_channel $chan_id2 --force
|
||||
new_blocks 1
|
||||
sleep 1
|
||||
new_blocks 150 # cltv before bob can broadcast
|
||||
sleep 5 # give bob time to process blocks and broadcast
|
||||
new_blocks 1 # confirm 2nd stage.
|
||||
sleep 1
|
||||
new_blocks 100 # deep
|
||||
sleep 5 # give bob time to fail incoming htlc
|
||||
unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent')
|
||||
if [[ "$unsettled" != "0" ]]; then
|
||||
echo 'alice htlc was not failed'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $1 == "just_in_time" ]]; then
|
||||
bob_node=$($bob nodeid)
|
||||
$alice setconfig zeroconf_trusted_node $bob_node
|
||||
|
||||
Reference in New Issue
Block a user