Skip to content

Commit

Permalink
htlctx: deal with possible peer htlctx batching
Browse files Browse the repository at this point in the history
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. Also, any input can now contain preimages which we have to
extract.
  • Loading branch information
bitromortac committed Nov 18, 2021
1 parent f9210b4 commit f4ebe6e
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 59 deletions.
11 changes: 5 additions & 6 deletions electrum/lnchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT,
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
Expand Down Expand Up @@ -502,8 +502,8 @@ def create_sweeptxs_for_our_ctx(self, ctx):
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
Expand Down Expand Up @@ -1541,9 +1541,8 @@ def force_close_tx(self) -> PartialTransaction:
assert tx.is_complete()
return tx

def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]:
# look at the output address, check if it matches
return tx_their_htlctx_justice(self, ctx, htlc_tx, self.sweep_address)
def maybe_sweep_revoked_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[int, SweepInfo]:
return txs_their_htlctx_justice(self, ctx, htlc_tx, self.sweep_address)

def has_pending_changes(self, subject: HTLCOwner) -> bool:
next_htlcs = self.hm.get_htlcs_in_next_ctx(subject)
Expand Down
96 changes: 60 additions & 36 deletions electrum/lnsweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

HTLC_TRANSACTION_DEADLINE_FRACTION = 4
HTLC_TRANSACTION_SWEEP_TARGET = 10
HTLCTX_INPUT_OUTPUT_INDEX = 0


class SweepInfo(NamedTuple):
Expand Down Expand Up @@ -171,21 +172,25 @@ def txs_htlc(
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_expiry=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_expiry=0,
Expand Down Expand Up @@ -306,16 +311,16 @@ def fee_estimator(size):
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,
Expand All @@ -339,15 +344,15 @@ def fee_estimator(size):

# sign HTLC output
remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc_relative_idx=htlc_relative_idx)
local_htlc_sig = bfh(final_htlc_tx.sign_txin(0, local_htlc_privkey))
txin = final_htlc_tx.inputs()[0]
local_htlc_sig = bfh(final_htlc_tx.sign_txin(HTLCTX_INPUT_OUTPUT_INDEX, local_htlc_privkey))
txin = final_htlc_tx.inputs()[HTLCTX_INPUT_OUTPUT_INDEX]
witness_program = bfh(Transaction.get_preimage_script(txin))
txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program)
return witness_script, final_htlc_tx


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
Expand All @@ -358,7 +363,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,
Expand Down Expand Up @@ -553,14 +558,14 @@ def txs_their_htlctx_justice(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction
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,
is_revocation=True,
config=chan.lnworker.config)

htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(
chan=chan,
ctx=ctx,
Expand Down Expand Up @@ -675,6 +680,7 @@ def tx_their_ctx_htlc(
cltv_expiry: int, config: SimpleConfig,
has_anchors: bool
) -> Optional[PartialTransaction]:
"""Deals with normal (non-CSV timelocked) HTLC output sweeps."""
assert type(cltv_expiry) is int
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
val = ctx.outputs()[output_idx].value
Expand Down Expand Up @@ -736,43 +742,61 @@ 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(
other_conf.revocation_basepoint.privkey,
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 = bh2u(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=bfh(witness_script),
privkey=other_revocation_privkey,
is_revocation=True,
config=chan.lnworker.config)
return SweepInfo(
name='redeem_htlc2',
csv_delay=0,
cltv_expiry=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 {}

index_to_sweepinfo = {}
for output_idx in htlc_outputs_idxs:
# generate justice transactions
gen_tx = lambda: tx_sweep_htlctx_output(
sweep_address=sweep_address,
output_idx=output_idx,
htlc_tx=htlc_tx,
htlctx_witness_script=bfh(witness_script),
privkey=other_revocation_privkey,
is_revocation=True,
config=chan.lnworker.config
)
index_to_sweepinfo[output_idx] = SweepInfo(
name='redeem_htlc2',
csv_delay=0,
cltv_expiry=0,
gen_tx=gen_tx
)

return index_to_sweepinfo
44 changes: 27 additions & 17 deletions electrum/lnwatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,36 +385,46 @@ async def sweep_commitment_transaction(self, funding_outpoint, closing_tx, spend
return False
chan_id_for_log = chan.get_id_for_log()
# detect who closed and get information about how to claim outputs
sweep_info_dict = chan.sweep_ctx(closing_tx)
sweep_info_dict = chan.sweep_ctx(closing_tx) # output -> SweepInfo
# spenders: output -> txid
keep_watching = False if sweep_info_dict else not self.is_deeply_mined(closing_tx.txid())

# create and broadcast transactions
for swept_output, sweep_info in sweep_info_dict.items():
for swept_output, sweep_info in sweep_info_dict.items(): # can be any sweep (l, r, htlc, second-htlc)
name = sweep_info.name + ' ' + chan.get_id_for_log()
# the output is swept by a certain txid that we know of
spender_txid = spenders.get(swept_output)
if spender_txid is not None:
# TODO: spender should be the htlc transaction, but could also be a to_local/to_remote sweep
# was output already swept and published?
spender_tx = self.db.get_transaction(spender_txid)
if not spender_tx:
keep_watching = True
continue
htlc_revocation_sweep_info = chan.maybe_sweep_revoked_htlc(closing_tx, spender_tx)
if htlc_revocation_sweep_info:

# TODO: type SweepInfos?
if not 'htlc' in name:
continue
# we check the scenario when the peer force closes and an HTLC transaction
# was published, whether the HTLC transaction includes revoked outputs
htlc_tx = spender_tx
htlc_txid = spender_txid

# check if we can extract preimages from an HTLC transaction
# a peer could have combined several HTLC-output spending inputs
for txin in htlc_tx.inputs():
chan.extract_preimage_from_htlc_txin(txin)
keep_watching |= not self.is_deeply_mined(htlc_txid)

# check if the HTLC transaction contains revoked outputs and redeem
htlc_idx_to_sweepinfo = chan.maybe_sweep_revoked_htlcs(closing_tx, htlc_tx)
for idx, htlc_revocation_sweep_info in htlc_idx_to_sweepinfo.items():
# check if we already redeemed revoked htlc
spender2 = spenders.get(spender_txid+':0')
if spender2:
keep_watching |= not self.is_deeply_mined(spender2)
htlc_tx_spender = spenders.get(spender_txid + f':{idx}')
if htlc_tx_spender:
keep_watching |= not self.is_deeply_mined(htlc_tx_spender)
else:
await self.try_redeem(spender_txid+':0', htlc_revocation_sweep_info, chan_id_for_log, name)
await self.try_redeem(spender_txid + f':{idx}', htlc_revocation_sweep_info, chan_id_for_log, name)
keep_watching = True
else:
# regular htlc transaction spending the htlc output of ctx,
# check if we can extract preimage from the input
keep_watching |= not self.is_deeply_mined(spender_txid)
txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(swept_output))
assert txin_idx is not None
spender_txin = spender_tx.inputs()[txin_idx]
chan.extract_preimage_from_htlc_txin(spender_txin)
else: # we sweep either the to_local, to_remote, or HTLC transaction outputs
await self.try_redeem(swept_output, sweep_info, chan_id_for_log, name)
keep_watching = True
Expand Down

0 comments on commit f4ebe6e

Please sign in to comment.