1
0

htlctx: deal with possible peer htlctx batching

Due to anchor channel's sighash.SINGLE and sighash.ANYONECANPAY,
several HTLC-transactions can be combined. This means we must watch for
revoked outputs in the HTLC transaction not only at index 0 but at any
index.
This commit is contained in:
bitromortac
2021-11-15 14:23:33 +01:00
committed by ThomasV
parent 9c277802e9
commit d535821516
3 changed files with 73 additions and 49 deletions

View File

@@ -58,7 +58,7 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT,
ChannelType, LNProtocolWarning, ctx_has_anchors)
from .lnsweep import txs_our_ctx, txs_their_ctx
from .lnsweep import tx_their_htlctx_justice, SweepInfo
from .lnsweep import txs_their_htlctx_justice, SweepInfo
from .lnsweep import tx_their_ctx_to_remote_backup
from .lnhtlc import HTLCManager
from .lnmsg import encode_msg, decode_msg
@@ -284,10 +284,10 @@ class AbstractChannel(Logger, ABC):
def delete_closing_height(self):
self.storage.pop('closing_height', None)
def create_sweeptxs_for_our_ctx(self, ctx):
def create_sweeptxs_for_our_ctx(self, ctx: Transaction) -> Optional[Dict[str, SweepInfo]]:
return txs_our_ctx(chan=self, ctx=ctx, sweep_address=self.get_sweep_address())
def create_sweeptxs_for_their_ctx(self, ctx):
def create_sweeptxs_for_their_ctx(self, ctx: Transaction) -> Optional[Dict[str, SweepInfo]]:
return txs_their_ctx(chan=self, ctx=ctx, sweep_address=self.get_sweep_address())
def is_backup(self) -> bool:
@@ -603,8 +603,8 @@ class ChannelBackup(AbstractChannel):
else:
return
def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]:
return None
def maybe_sweep_revoked_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[int, SweepInfo]:
return {}
def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None:
return None
@@ -1726,9 +1726,9 @@ class Channel(AbstractChannel):
assert not (self.get_state() == ChannelState.WE_ARE_TOXIC and ChanCloseOption.LOCAL_FCLOSE in ret), "local force-close unsafe if we are toxic"
return ret
def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]:
def maybe_sweep_revoked_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[int, SweepInfo]:
# look at the output address, check if it matches
return tx_their_htlctx_justice(self, ctx, htlc_tx, self.get_sweep_address())
return txs_their_htlctx_justice(self, ctx, htlc_tx, self.get_sweep_address())
def has_pending_changes(self, subject: HTLCOwner) -> bool:
next_htlcs = self.hm.get_htlcs_in_next_ctx(subject)

View File

@@ -4,6 +4,7 @@
from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Callable
from enum import Enum, auto
from functools import partial
import electrum_ecc as ecc
@@ -36,6 +37,7 @@ _logger = get_logger(__name__)
HTLC_TRANSACTION_DEADLINE_FRACTION = 4
HTLC_TRANSACTION_SWEEP_TARGET = 10
HTLCTX_INPUT_OUTPUT_INDEX = 0
class SweepInfo(NamedTuple):
@@ -90,8 +92,9 @@ def txs_their_ctx_watchtower(chan: 'Channel', ctx: Transaction, per_commitment_s
commit=ctx,
htlc=htlc,
ctx_output_idx=ctx_output_idx)
return tx_sweep_our_htlctx(
return tx_sweep_htlctx_output(
htlc_tx=htlc_tx,
output_idx=HTLCTX_INPUT_OUTPUT_INDEX,
htlctx_witness_script=htlc_tx_witness_script,
sweep_address=sweep_address,
privkey=other_revocation_privkey,
@@ -147,19 +150,23 @@ def tx_their_ctx_justice(
return None
def tx_their_htlctx_justice(
def txs_their_htlctx_justice(
chan: 'Channel',
ctx: Transaction,
htlc_tx: Transaction,
sweep_address: str) -> Optional[SweepInfo]:
sweep_address: str) -> Dict[int, SweepInfo]:
"""Creates justice transactions for every output in the HTLC transaction.
Due to anchor type channels it can happen that a remote party batches HTLC transactions,
which is why this method can return multiple SweepInfos.
"""
x = extract_ctx_secrets(chan, ctx)
if not x:
return
return {}
ctn, their_pcp, is_revocation, per_commitment_secret = x
if not is_revocation:
return
# prep
return {}
# get HTLC constraints (secrets and locktime)
pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False)
other_revocation_privkey = derive_blinded_privkey(
@@ -167,26 +174,38 @@ def tx_their_htlctx_justice(
per_commitment_secret)
to_self_delay = other_conf.to_self_delay
this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp)
# same witness script as to_local
revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True)
# uses the same witness script as to_local
witness_script = make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, this_delayed_pubkey)
htlc_address = redeem_script_to_address('p2wsh', witness_script)
# check that htlc_tx is a htlc
if htlc_tx.outputs()[0].address != htlc_address:
return
gen_tx = lambda: tx_sweep_our_htlctx(
sweep_address=sweep_address,
htlc_tx=htlc_tx,
htlctx_witness_script=witness_script,
privkey=other_revocation_privkey,
is_revocation=True,
config=chan.lnworker.config)
return SweepInfo(
name='redeem_htlc2',
csv_delay=0,
cltv_abs=0,
gen_tx=gen_tx)
# check that htlc transaction contains at least an output that is supposed to be
# spent via a second stage htlc transaction
htlc_outputs_idxs = [idx for idx, output in enumerate(htlc_tx.outputs()) if output.address == htlc_address]
if not htlc_outputs_idxs:
return {}
# generate justice transactions
def justice_tx(output_idx):
return tx_sweep_htlctx_output(
sweep_address=sweep_address,
output_idx=output_idx,
htlc_tx=htlc_tx,
htlctx_witness_script=witness_script,
privkey=other_revocation_privkey,
is_revocation=True,
config=chan.lnworker.config
)
index_to_sweepinfo = {}
for output_idx in htlc_outputs_idxs:
index_to_sweepinfo[output_idx] = SweepInfo(
name='redeem_htlc2',
csv_delay=0,
cltv_abs=0,
gen_tx=partial(justice_tx, output_idx)
)
return index_to_sweepinfo
def txs_our_ctx(
@@ -281,21 +300,25 @@ def txs_our_ctx(
htlc_direction=htlc_direction,
ctx_output_idx=ctx_output_idx,
htlc_relative_idx=htlc_relative_idx)
sweep_tx = lambda: tx_sweep_our_htlctx(
# we sweep our ctx with HTLC transactions individually, therefore the CSV-locked output is always at
# index TIMELOCKED_HTLCTX_OUTPUT_INDEX
assert True
sweep_tx = lambda: tx_sweep_htlctx_output(
to_self_delay=to_self_delay,
htlc_tx=htlc_tx,
output_idx=HTLCTX_INPUT_OUTPUT_INDEX,
htlctx_witness_script=htlctx_witness_script,
sweep_address=sweep_address,
privkey=our_localdelayed_privkey.get_secret_bytes(),
is_revocation=False,
config=chan.lnworker.config)
# side effect
txs[htlc_tx.inputs()[0].prevout.to_str()] = SweepInfo(
txs[htlc_tx.inputs()[HTLCTX_INPUT_OUTPUT_INDEX].prevout.to_str()] = SweepInfo(
name='first-stage-htlc',
csv_delay=0,
cltv_abs=htlc_tx.locktime,
gen_tx=lambda: htlc_tx)
txs[htlc_tx.txid() + ':0'] = SweepInfo(
txs[htlc_tx.txid() + f':{HTLCTX_INPUT_OUTPUT_INDEX}'] = SweepInfo(
name='second-stage-htlc',
csv_delay=to_self_delay,
cltv_abs=0,
@@ -645,16 +668,16 @@ def tx_our_ctx_htlctx(
htlc_outpoint = TxOutpoint(txid=bfh(ctx.txid()), out_idx=ctx_output_idx)
htlc_input_idx = funded_htlc_tx.get_input_idx_that_spent_prevout(htlc_outpoint)
htlc_out_address = maybe_zero_fee_htlc_tx.outputs()[0].address
htlc_out_address = maybe_zero_fee_htlc_tx.outputs()[HTLCTX_INPUT_OUTPUT_INDEX].address
htlc_output_idx = funded_htlc_tx.get_output_idxs_from_address(htlc_out_address).pop()
inputs = funded_htlc_tx.inputs()
outputs = funded_htlc_tx.outputs()
if htlc_input_idx != 0:
if htlc_input_idx != HTLCTX_INPUT_OUTPUT_INDEX:
htlc_txin = inputs.pop(htlc_input_idx)
inputs.insert(0, htlc_txin)
if htlc_output_idx != 0:
inputs.insert(HTLCTX_INPUT_OUTPUT_INDEX, htlc_txin)
if htlc_output_idx != HTLCTX_INPUT_OUTPUT_INDEX:
htlc_txout = outputs.pop(htlc_output_idx)
outputs.insert(0, htlc_txout)
outputs.insert(HTLCTX_INPUT_OUTPUT_INDEX, htlc_txout)
final_htlc_tx = PartialTransaction.from_io(
inputs,
outputs,
@@ -677,8 +700,8 @@ def tx_our_ctx_htlctx(
# sign HTLC output
remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc_relative_idx=htlc_relative_idx)
local_htlc_sig = final_htlc_tx.sign_txin(0, local_htlc_privkey)
txin = final_htlc_tx.inputs()[0]
local_htlc_sig = final_htlc_tx.sign_txin(HTLCTX_INPUT_OUTPUT_INDEX, local_htlc_privkey)
txin = final_htlc_tx.inputs()[HTLCTX_INPUT_OUTPUT_INDEX]
witness_script_in = txin.witness_script
assert witness_script_in
txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_script_in)
@@ -693,6 +716,7 @@ def tx_their_ctx_htlc(
config: SimpleConfig,
has_anchors: bool,
) -> Optional[PartialTransaction]:
"""Deals with normal (non-CSV timelocked) HTLC output sweeps."""
assert type(cltv_abs) is int
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
val = ctx.outputs()[output_idx].value
@@ -801,8 +825,8 @@ def tx_ctx_to_local(
def tx_sweep_our_htlctx(
*, htlc_tx: Transaction, htlctx_witness_script: bytes, sweep_address: str,
def tx_sweep_htlctx_output(
*, htlc_tx: Transaction, output_idx: int, htlctx_witness_script: bytes, sweep_address: str,
privkey: bytes, is_revocation: bool, to_self_delay: int = None,
config: SimpleConfig) -> Optional[PartialTransaction]:
"""Create a txn that sweeps the output of a first stage htlc tx
@@ -813,7 +837,7 @@ def tx_sweep_our_htlctx(
return tx_ctx_to_local(
sweep_address=sweep_address,
ctx=htlc_tx,
output_idx=0,
output_idx=output_idx,
witness_script=htlctx_witness_script,
privkey=privkey,
is_revocation=is_revocation,

View File

@@ -463,14 +463,14 @@ class LNWalletWatcher(LNWatcher):
spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None
if spender_tx:
# the spender might be the remote, revoked or not
e_htlc_tx = chan.maybe_sweep_revoked_htlc(closing_tx, spender_tx)
if e_htlc_tx:
spender2 = spenders.get(spender_txid+':0')
if spender2:
keep_watching |= not self.is_deeply_mined(spender2)
htlc_idx_to_sweepinfo = chan.maybe_sweep_revoked_htlcs(closing_tx, spender_tx)
for idx, htlc_revocation_sweep_info in htlc_idx_to_sweepinfo.items():
htlc_tx_spender = spenders.get(spender_txid+f':{idx}')
if htlc_tx_spender:
keep_watching |= not self.is_deeply_mined(htlc_tx_spender)
else:
keep_watching = True
await self.maybe_redeem(spenders, spender_txid+':0', e_htlc_tx, name)
await self.maybe_redeem(spenders, spender_txid+f':{idx}', htlc_revocation_sweep_info, name)
else:
keep_watching |= not self.is_deeply_mined(spender_txid)
txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout))