1
0

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:
ThomasV
2025-03-11 18:13:32 +01:00
parent 25e1f51f65
commit 798df671ea
5 changed files with 124 additions and 33 deletions

View File

@@ -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

View File

@@ -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()),
)

View File

@@ -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)"""

View File

@@ -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': {

View File

@@ -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