1
0
Files
electrum/electrum/lnsweep.py
f321x 32aa6ab20c lnutil: rename RecvMPPResolution.ACCEPTED
Renames RecvMPPResolution.ACCEPTED to .COMPLETE as .ACCEPTED is somewhat
misleading. Accepted could imply that the preimage for this set has been
revealed or that the set has been settled, however it only means that we
have received the full set (it is complete), but the set still can be
failed (e.g. through cltv timeout) and has not been claimed yet.
2025-09-29 16:11:26 +00:00

901 lines
37 KiB
Python

# Copyright (C) 2018 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Callable
import electrum_ecc as ecc
from .util import bfh, UneconomicFee
from .crypto import privkey_to_pubkey
from .bitcoin import redeem_script_to_address, construct_witness
from . import descriptor
from . import bitcoin
from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script,
derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey,
make_htlc_tx_witness, make_htlc_tx_with_open_channel, UpdateAddHtlc,
LOCAL, REMOTE, make_htlc_output_witness_script,
get_ordered_channel_configs, get_per_commitment_secret_from_seed,
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script,
derive_payment_basepoint, ctx_has_anchors, SCRIPT_TEMPLATE_FUNDING, Keypair,
derive_multisig_funding_key_if_we_opened, derive_multisig_funding_key_if_they_opened)
from .transaction import (Transaction, TxInput, PartialTxInput,
PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template)
from .logging import get_logger, Logger
if TYPE_CHECKING:
from .lnchannel import Channel, AbstractChannel, ChannelBackup
_logger = get_logger(__name__)
# note: better to use chan.logger instead, when applicable
HTLC_TRANSACTION_DEADLINE_FRACTION = 4
HTLC_TRANSACTION_SWEEP_TARGET = 10
HTLCTX_INPUT_OUTPUT_INDEX = 0
class SweepInfo(NamedTuple):
name: str
cltv_abs: Optional[int] # set to None only if the script has no cltv
txin: PartialTxInput
txout: Optional[PartialTxOutput] # only for first-stage htlc tx
can_be_batched: bool # todo: this could be more fine-grained
def is_anchor(self):
return self.name in ['local_anchor', 'remote_anchor']
@property
def csv_delay(self):
return self.txin.get_block_based_relative_locktime() or 0
def sweep_their_ctx_watchtower(
chan: 'Channel',
ctx: Transaction,
per_commitment_secret: bytes
) -> List[PartialTxInput]:
"""Presign sweeping transactions using the just received revoked pcs.
These will only be utilised if the remote breaches.
Sweep 'to_local', and all the HTLCs (two cases: directly from ctx, or from HTLC tx).
"""
# prep
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
breacher_conf, watcher_conf = get_ordered_channel_configs(chan=chan, for_us=False)
watcher_revocation_privkey = derive_blinded_privkey(
watcher_conf.revocation_basepoint.privkey,
per_commitment_secret
)
to_self_delay = watcher_conf.to_self_delay
breacher_delayed_pubkey = derive_pubkey(breacher_conf.delayed_basepoint.pubkey, pcp)
txins = []
# create justice tx for breacher's to_local output
revocation_pubkey = ecc.ECPrivkey(watcher_revocation_privkey).get_public_key_bytes(compressed=True)
witness_script = make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, breacher_delayed_pubkey)
to_local_address = redeem_script_to_address('p2wsh', witness_script)
output_idxs = ctx.get_output_idxs_from_address(to_local_address)
if output_idxs:
output_idx = output_idxs.pop()
txin = sweep_ctx_to_local(
ctx=ctx,
output_idx=output_idx,
witness_script=witness_script,
privkey=watcher_revocation_privkey,
is_revocation=True,
)
if txin:
txins.append(txin)
# create justice txs for breacher's HTLC outputs
breacher_htlc_pubkey = derive_pubkey(breacher_conf.htlc_basepoint.pubkey, pcp)
watcher_htlc_pubkey = derive_pubkey(watcher_conf.htlc_basepoint.pubkey, pcp)
def txin_htlc(
htlc: 'UpdateAddHtlc', is_received_htlc: bool,
ctx_output_idx: int) -> None:
htlc_output_witness_script = make_htlc_output_witness_script(
is_received_htlc=is_received_htlc,
remote_revocation_pubkey=revocation_pubkey,
remote_htlc_pubkey=watcher_htlc_pubkey,
local_htlc_pubkey=breacher_htlc_pubkey,
payment_hash=htlc.payment_hash,
cltv_abs=htlc.cltv_abs,
has_anchors=chan.has_anchors()
)
cltv_abs = htlc.cltv_abs if is_received_htlc else 0
return sweep_their_ctx_htlc(
ctx=ctx,
witness_script=htlc_output_witness_script,
preimage=None,
output_idx=ctx_output_idx,
privkey=watcher_revocation_privkey,
is_revocation=True,
cltv_abs=cltv_abs,
has_anchors=chan.has_anchors()
)
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(
chan=chan,
ctx=ctx,
pcp=pcp,
subject=REMOTE,
ctn=ctn)
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
txins.append(
txin_htlc(
htlc=htlc,
is_received_htlc=direction == RECEIVED,
ctx_output_idx=ctx_output_idx)
)
# for anchor channels we don't know the HTLC transaction's txid beforehand due
# to malleability because of ANYONECANPAY
if chan.has_anchors():
return txins
# create justice transactions for HTLC transaction's outputs
def sweep_their_htlctx_justice(
*,
htlc: 'UpdateAddHtlc',
htlc_direction: Direction,
ctx_output_idx: int
) -> Optional[PartialTxInput]:
htlc_tx_witness_script, htlc_tx = make_htlc_tx_with_open_channel(
chan=chan,
pcp=pcp,
subject=REMOTE,
ctn=ctn,
htlc_direction=htlc_direction,
commit=ctx,
htlc=htlc,
ctx_output_idx=ctx_output_idx)
return sweep_htlctx_output(
htlc_tx=htlc_tx,
output_idx=HTLCTX_INPUT_OUTPUT_INDEX,
htlctx_witness_script=htlc_tx_witness_script,
privkey=watcher_revocation_privkey,
is_revocation=True,
)
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(
chan=chan,
ctx=ctx,
pcp=pcp,
subject=REMOTE,
ctn=ctn)
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
secondstage_sweep_tx = sweep_their_htlctx_justice(
htlc=htlc,
htlc_direction=direction,
ctx_output_idx=ctx_output_idx)
if secondstage_sweep_tx:
txins.append(secondstage_sweep_tx)
return txins
def sweep_their_ctx_justice(
chan: 'Channel',
ctx: Transaction,
per_commitment_secret: bytes,
) -> Optional[PartialTxInput]:
# prep
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)
# to_local
revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True)
witness_script = make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, this_delayed_pubkey)
to_local_address = redeem_script_to_address('p2wsh', witness_script)
output_idxs = ctx.get_output_idxs_from_address(to_local_address)
if output_idxs:
output_idx = output_idxs.pop()
sweep_txin = sweep_ctx_to_local(
ctx=ctx,
output_idx=output_idx,
witness_script=witness_script,
privkey=other_revocation_privkey,
is_revocation=True,
)
return sweep_txin
return None
def sweep_their_htlctx_justice(
chan: 'Channel',
ctx: Transaction,
htlc_tx: Transaction,
) -> Dict[str, 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 {}
ctn, their_pcp, is_revocation, per_commitment_secret = x
if not is_revocation:
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)
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 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_txin(output_idx):
return sweep_htlctx_output(
output_idx=output_idx,
htlc_tx=htlc_tx,
htlctx_witness_script=witness_script,
privkey=other_revocation_privkey,
is_revocation=True,
)
index_to_sweepinfo = {}
for output_idx in htlc_outputs_idxs:
if txin := justice_txin(output_idx):
prevout = htlc_tx.txid() + f':{output_idx}'
index_to_sweepinfo[prevout] = SweepInfo(
name=f'second-stage-htlc:{output_idx}',
cltv_abs=None,
txin=txin,
txout=None,
can_be_batched=False,
)
return index_to_sweepinfo
def sweep_our_htlctx(
chan: 'AbstractChannel',
ctx: Transaction,
htlc_tx: Transaction):
txs = sweep_our_ctx(
chan=chan,
ctx=ctx,
actual_htlc_tx=htlc_tx)
return txs
def sweep_our_ctx(
*, chan: 'AbstractChannel',
ctx: Transaction,
actual_htlc_tx: Transaction=None, # if passed, return second stage htlcs
) -> Dict[str, SweepInfo]:
"""Handle the case where we force-close unilaterally with our latest ctx.
We sweep:
to_local: CSV delayed
htlc success: CSV delay with anchors, no delay otherwise
htlc timeout: CSV delay with anchors, CLTV locktime
second-stage htlc transactions: CSV delay
'to_local' can be swept even if this is a breach (by us),
but HTLCs cannot (old HTLCs are no longer stored).
Outputs with CSV/CLTV are redeemed by LNWatcher.
"""
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
our_per_commitment_secret = get_per_commitment_secret_from_seed(
our_conf.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
our_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True)
our_delayed_bp_privkey = ecc.ECPrivkey(our_conf.delayed_basepoint.privkey)
our_localdelayed_privkey = derive_privkey(our_delayed_bp_privkey.secret_scalar, our_pcp)
our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey)
their_revocation_pubkey = derive_blinded_pubkey(their_conf.revocation_basepoint.pubkey, our_pcp)
to_self_delay = their_conf.to_self_delay
our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'),
per_commitment_point=our_pcp).to_bytes(32, 'big')
our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)
to_local_witness_script = make_commitment_output_to_local_witness_script(
their_revocation_pubkey, to_self_delay, our_localdelayed_pubkey)
to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script)
to_remote_address = None
# test if this is our_ctx
found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address))
if not chan.is_backup():
assert chan.is_static_remotekey_enabled()
their_payment_pubkey = their_conf.payment_basepoint.pubkey
to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey, has_anchors=chan.has_anchors())
found_to_remote = bool(ctx.get_output_idxs_from_address(to_remote_address))
else:
found_to_remote = False
if not found_to_local and not found_to_remote:
return {}
#chan.logger.debug(f'(lnsweep) found our ctx: {to_local_address} {to_remote_address}')
# other outputs are htlcs
# if they are spent, we need to generate the script
# so, second-stage htlc sweep should not be returned here
txs = {} # type: Dict[str, SweepInfo]
# local anchor
if actual_htlc_tx is None and chan.has_anchors():
if txin := sweep_ctx_anchor(ctx=ctx, multisig_key=our_conf.multisig_key):
txs[txin.prevout.to_str()] = SweepInfo(
name='local_anchor',
cltv_abs=None,
txin=txin,
txout=None,
can_be_batched=True,
)
# to_local
output_idxs = ctx.get_output_idxs_from_address(to_local_address)
if actual_htlc_tx is None and output_idxs:
output_idx = output_idxs.pop()
if txin := sweep_ctx_to_local(
ctx=ctx,
output_idx=output_idx,
witness_script=to_local_witness_script,
privkey=our_localdelayed_privkey.get_secret_bytes(),
is_revocation=False,
to_self_delay=to_self_delay,
):
prevout = ctx.txid() + ':%d'%output_idx
txs[prevout] = SweepInfo(
name='our_ctx_to_local',
cltv_abs=None,
txin=txin,
txout=None,
can_be_batched=True,
)
we_breached = ctn < chan.get_oldest_unrevoked_ctn(LOCAL)
if we_breached:
chan.logger.info(f"(lnsweep) we breached. txid: {ctx.txid()}")
# return only our_ctx_to_local, because we don't keep htlc_signatures for old states
return txs
# HTLCs
def txs_htlc(
*, htlc: 'UpdateAddHtlc',
htlc_direction: Direction,
ctx_output_idx: int,
htlc_relative_idx,
preimage: Optional[bytes]):
htlctx_witness_script, htlc_tx = tx_our_ctx_htlctx(
chan=chan,
our_pcp=our_pcp,
ctx=ctx,
htlc=htlc,
local_htlc_privkey=our_htlc_privkey,
preimage=preimage,
htlc_direction=htlc_direction,
ctx_output_idx=ctx_output_idx,
htlc_relative_idx=htlc_relative_idx)
if actual_htlc_tx is None:
name = 'offered-htlc' if htlc_direction == SENT else 'received-htlc'
prevout = ctx.txid() + f':{ctx_output_idx}'
txs[prevout] = SweepInfo(
name=name,
cltv_abs=htlc_tx.locktime,
txin=htlc_tx.inputs()[0],
txout=htlc_tx.outputs()[0],
can_be_batched=False, # both parties can spend
# - actually, we might want to batch depending on the context
# f(amount in htlc, remaining_time, number of available utxos for anchors)
# - in particular, it would be safe to batch htlcs where
# htlc_direction, htlc.payment_hash, htlc.cltv_abs
# all match. That is, MPP htlcs for the same payment.
)
else:
# second-stage
address = bitcoin.script_to_p2wsh(htlctx_witness_script)
output_idxs = actual_htlc_tx.get_output_idxs_from_address(address)
for output_idx in output_idxs:
if sweep_txin := sweep_htlctx_output(
to_self_delay=to_self_delay,
htlc_tx=actual_htlc_tx,
output_idx=output_idx,
htlctx_witness_script=htlctx_witness_script,
privkey=our_localdelayed_privkey.get_secret_bytes(),
is_revocation=False,
):
txs[actual_htlc_tx.txid() + f':{output_idx}'] = SweepInfo(
name=f'second-stage-htlc:{output_idx}',
cltv_abs=0,
txin=sweep_txin,
txout=None,
# this is safe to batch, we are the only ones who can spend
# (assuming we did not broadcast a revoked state)
can_be_batched=True,
)
# offered HTLCs, in our ctx --> "timeout"
# received HTLCs, in our ctx --> "success"
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(
chan=chan,
ctx=ctx,
pcp=our_pcp,
subject=LOCAL,
ctn=ctn)
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
if direction == RECEIVED:
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
# do not redeem this, it might publish the preimage of an incomplete MPP
continue
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
if not preimage:
# we might not have the preimage if this is a hold invoice
continue
else:
preimage = None
try:
txs_htlc(
htlc=htlc,
htlc_direction=direction,
ctx_output_idx=ctx_output_idx,
htlc_relative_idx=htlc_relative_idx,
preimage=preimage)
except UneconomicFee:
continue
return txs
def extract_ctx_secrets(chan: 'Channel', ctx: Transaction):
# note: the remote sometimes has two valid non-revoked commitment transactions,
# either of which could be broadcast
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
per_commitment_secret = None
oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)
if ctn == oldest_unrevoked_remote_ctn:
their_pcp = their_conf.current_per_commitment_point
is_revocation = False
elif ctn == oldest_unrevoked_remote_ctn + 1:
their_pcp = their_conf.next_per_commitment_point
is_revocation = False
elif ctn < oldest_unrevoked_remote_ctn: # breach
try:
per_commitment_secret = chan.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
except UnableToDeriveSecret:
return
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
is_revocation = True
#chan.logger.debug(f'(lnsweep) tx for revoked: {list(txs.keys())}')
elif chan.get_data_loss_protect_remote_pcp(ctn):
their_pcp = chan.get_data_loss_protect_remote_pcp(ctn)
is_revocation = False
else:
return
return ctn, their_pcp, is_revocation, per_commitment_secret
def extract_funding_pubkeys_from_ctx(txin: TxInput) -> Tuple[bytes, bytes]:
"""Extract the two funding pubkeys from the published commitment transaction.
We expect to see a witness script of: OP_2 pk1 pk2 OP_2 OP_CHECKMULTISIG"""
elements = txin.witness_elements()
witness_script = elements[-1]
assert match_script_against_template(witness_script, SCRIPT_TEMPLATE_FUNDING)
parsed_script = [x for x in script_GetOp(witness_script)]
pubkey1 = parsed_script[1][1]
pubkey2 = parsed_script[2][1]
return (pubkey1, pubkey2)
def sweep_their_ctx_to_remote_backup(
*, chan: 'ChannelBackup',
ctx: Transaction,
funding_tx: Transaction,
) -> Optional[Dict[str, SweepInfo]]:
txs = {} # type: Dict[str, SweepInfo]
"""If we only have a backup, and the remote force-closed with their ctx,
and anchors are enabled, we need to sweep to_remote."""
if ctx_has_anchors(ctx):
# for anchors we need to sweep to_remote
funding_pubkeys = extract_funding_pubkeys_from_ctx(ctx.inputs()[0])
_logger.debug(f'checking their ctx for funding pubkeys: {[pk.hex() for pk in funding_pubkeys]}')
# check which of the pubkey was ours
for fp_idx, pubkey in enumerate(funding_pubkeys):
candidate_basepoint = derive_payment_basepoint(chan.lnworker.static_payment_key.privkey, funding_pubkey=pubkey)
candidate_to_remote_address = make_commitment_output_to_remote_address(candidate_basepoint.pubkey, has_anchors=True)
if ctx.get_output_idxs_from_address(candidate_to_remote_address):
our_payment_pubkey = candidate_basepoint
to_remote_address = candidate_to_remote_address
_logger.debug(f'found funding pubkey')
break
else:
return
else:
# we are dealing with static_remotekey which is locked to a wallet address
return {}
# remote anchor
# derive funding_privkey ("multisig_key")
# note: for imported backups, we already have this as 'local_config.multisig_key'
# but for on-chain backups, we need to derive it.
# For symmetry, we derive it now regardless of type
our_funding_pubkey = funding_pubkeys[fp_idx]
their_funding_pubkey = funding_pubkeys[1 - fp_idx]
remote_node_id = chan.node_id # for onchain backups, this is only the prefix
if chan.is_initiator():
funding_kp_cand = derive_multisig_funding_key_if_we_opened(
funding_root_secret=chan.lnworker.funding_root_keypair.privkey,
remote_node_id_or_prefix=remote_node_id,
nlocktime=funding_tx.locktime,
)
else:
funding_kp_cand = derive_multisig_funding_key_if_they_opened(
funding_root_secret=chan.lnworker.funding_root_keypair.privkey,
remote_node_id_or_prefix=remote_node_id,
remote_funding_pubkey=their_funding_pubkey,
)
assert funding_kp_cand.pubkey == our_funding_pubkey, f"funding pubkey mismatch1. {chan.is_initiator()=}"
our_ms_funding_keypair = funding_kp_cand
# sanity check funding_privkey, if we had it already (if backup is imported):
if local_config := chan.config.get(LOCAL):
assert our_ms_funding_keypair == local_config.multisig_key, f"funding pubkey mismatch2. {chan.is_initiator()=}"
if our_ms_funding_keypair:
if txin := sweep_ctx_anchor(ctx=ctx, multisig_key=our_ms_funding_keypair):
txs[txin.prevout.to_str()] = SweepInfo(
name='remote_anchor',
cltv_abs=None,
txin=txin,
txout=None,
can_be_batched=True,
)
# to_remote
our_payment_privkey = ecc.ECPrivkey(our_payment_pubkey.privkey)
output_idxs = ctx.get_output_idxs_from_address(to_remote_address)
if output_idxs:
output_idx = output_idxs.pop()
prevout = ctx.txid() + ':%d' % output_idx
if txin := sweep_their_ctx_to_remote(
ctx=ctx,
output_idx=output_idx,
our_payment_privkey=our_payment_privkey,
has_anchors=True
):
txs[prevout] = SweepInfo(
name='their_ctx_to_remote_backup',
cltv_abs=None,
txin=txin,
txout=None,
can_be_batched=True,
)
return txs
def sweep_their_ctx(
*, chan: 'Channel',
ctx: Transaction) -> Optional[Dict[str, SweepInfo]]:
"""Handle the case when the remote force-closes with their ctx.
Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs).
Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher.
We sweep:
to_local: if revoked
to_remote: CSV delay with anchors, otherwise sweeping not needed
htlc success: CSV delay with anchors, no delay otherwise, or revoked
htlc timeout: CSV delay with anchors, CLTV locktime, or revoked
second-stage htlc transactions: CSV delay
Outputs with CSV/CLTV are redeemed by LNWatcher.
"""
txs = {} # type: Dict[str, SweepInfo]
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
x = extract_ctx_secrets(chan, ctx)
if not x:
return
ctn, their_pcp, is_revocation, per_commitment_secret = x
# to_local
our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp)
their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp)
witness_script = make_commitment_output_to_local_witness_script(
our_revocation_pubkey, our_conf.to_self_delay, their_delayed_pubkey)
to_local_address = redeem_script_to_address('p2wsh', witness_script)
to_remote_address = None
# test if this is their ctx
found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address))
if not chan.is_backup():
assert chan.is_static_remotekey_enabled()
our_payment_pubkey = our_conf.payment_basepoint.pubkey
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=chan.has_anchors())
found_to_remote = bool(ctx.get_output_idxs_from_address(to_remote_address))
else:
found_to_remote = False
if not found_to_local and not found_to_remote:
return
chan.logger.debug(f'(lnsweep) found their ctx: {to_local_address} {to_remote_address}')
# remote anchor
if chan.has_anchors():
if txin := sweep_ctx_anchor(ctx=ctx, multisig_key=our_conf.multisig_key):
txs[txin.prevout.to_str()] = SweepInfo(
name='remote_anchor',
cltv_abs=None,
txin=txin,
txout=None,
can_be_batched=True,
)
# to_local is handled by lnwatcher
if is_revocation:
our_revocation_privkey = derive_blinded_privkey(our_conf.revocation_basepoint.privkey, per_commitment_secret)
if txin := sweep_their_ctx_justice(chan, ctx, per_commitment_secret):
txs[txin.prevout.to_str()] = SweepInfo(
name='to_local_for_revoked_ctx',
cltv_abs=None,
txin=txin,
txout=None,
can_be_batched=False,
)
# to_remote
if chan.has_anchors():
sweep_to_remote = True
our_payment_privkey = ecc.ECPrivkey(our_conf.payment_basepoint.privkey)
else:
assert chan.is_static_remotekey_enabled()
sweep_to_remote = False
our_payment_privkey = None
if sweep_to_remote:
assert our_payment_pubkey == our_payment_privkey.get_public_key_bytes(compressed=True)
output_idxs = ctx.get_output_idxs_from_address(to_remote_address)
if output_idxs:
output_idx = output_idxs.pop()
prevout = ctx.txid() + ':%d' % output_idx
if txin := sweep_their_ctx_to_remote(
ctx=ctx,
output_idx=output_idx,
our_payment_privkey=our_payment_privkey,
has_anchors=chan.has_anchors()
):
# todo: we might not want to sweep this at all, if we add it to the wallet addresses
txs[prevout] = SweepInfo(
name='their_ctx_to_remote',
cltv_abs=None,
txin=txin,
txout=None,
can_be_batched=True,
)
# HTLCs
our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp)
our_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(our_htlc_privkey)
their_htlc_pubkey = derive_pubkey(their_conf.htlc_basepoint.pubkey, their_pcp)
def tx_htlc(
*, htlc: 'UpdateAddHtlc',
is_received_htlc: bool,
ctx_output_idx: int,
preimage: Optional[bytes]) -> None:
htlc_output_witness_script = make_htlc_output_witness_script(
is_received_htlc=is_received_htlc,
remote_revocation_pubkey=our_revocation_pubkey,
remote_htlc_pubkey=our_htlc_privkey.get_public_key_bytes(compressed=True),
local_htlc_pubkey=their_htlc_pubkey,
payment_hash=htlc.payment_hash,
cltv_abs=htlc.cltv_abs,
has_anchors=chan.has_anchors())
cltv_abs = htlc.cltv_abs if is_received_htlc and not is_revocation else 0
prevout = ctx.txid() + ':%d'%ctx_output_idx
if txin := sweep_their_ctx_htlc(
ctx=ctx,
witness_script=htlc_output_witness_script,
preimage=preimage,
output_idx=ctx_output_idx,
privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),
is_revocation=is_revocation,
cltv_abs=cltv_abs,
has_anchors=chan.has_anchors(),
):
txs[prevout] = SweepInfo(
name=f'their_ctx_htlc_{ctx_output_idx}{"_for_revoked_ctx" if is_revocation else ""}',
cltv_abs=cltv_abs,
txin=txin,
txout=None,
can_be_batched=False, # both parties can spend
# (still, in some cases we could batch, see comment in sweep_our_ctx)
)
# received HTLCs, in their ctx --> "timeout"
# offered HTLCs, in their ctx --> "success"
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(
chan=chan,
ctx=ctx,
pcp=their_pcp,
subject=REMOTE,
ctn=ctn)
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
is_received_htlc = direction == RECEIVED
if not is_received_htlc and not is_revocation:
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
# do not redeem this, it might publish the preimage of an incomplete MPP
continue
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
if not preimage:
# we might not have the preimage if this is a hold invoice
continue
else:
preimage = None
tx_htlc(
htlc=htlc,
is_received_htlc=is_received_htlc,
ctx_output_idx=ctx_output_idx,
preimage=preimage)
return txs
def tx_our_ctx_htlctx(
chan: 'Channel',
our_pcp: bytes,
ctx: Transaction,
htlc: 'UpdateAddHtlc',
local_htlc_privkey: bytes,
preimage: Optional[bytes],
htlc_direction: Direction,
htlc_relative_idx: int,
ctx_output_idx: int) -> Tuple[bytes, Transaction]:
assert (htlc_direction == RECEIVED) == bool(preimage), 'preimage is required iff htlc is received'
preimage = preimage or b''
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
witness_script_out, maybe_zero_fee_htlc_tx = make_htlc_tx_with_open_channel(
chan=chan,
pcp=our_pcp,
subject=LOCAL,
ctn=ctn,
htlc_direction=htlc_direction,
commit=ctx,
htlc=htlc,
ctx_output_idx=ctx_output_idx,
name=f'our_ctx_{ctx_output_idx}_htlc_tx_{htlc.payment_hash.hex()}')
# sign HTLC output
remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc_relative_idx=htlc_relative_idx)
txin = maybe_zero_fee_htlc_tx.inputs()[HTLCTX_INPUT_OUTPUT_INDEX]
witness_script_in = txin.witness_script
assert witness_script_in
txin.privkey = local_htlc_privkey
txin.make_witness = lambda local_htlc_sig: make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_script_in)
return witness_script_out, maybe_zero_fee_htlc_tx
def sweep_their_ctx_htlc(
ctx: Transaction,
witness_script: bytes,
preimage: Optional[bytes], output_idx: int,
privkey: bytes, is_revocation: bool,
cltv_abs: int,
has_anchors: bool,
) -> Optional[PartialTxInput]:
"""Deals with normal (non-CSV timelocked) HTLC output sweeps."""
assert type(cltv_abs) is int
assert witness_script is not None
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
val = ctx.outputs()[output_idx].value
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
txin = PartialTxInput(prevout=prevout)
txin._trusted_value_sats = val
txin.witness_script = witness_script
txin.script_sig = b''
txin.nsequence = 1 if has_anchors else 0xffffffff - 2
txin.privkey = privkey
if not is_revocation:
txin.make_witness = lambda sig: construct_witness([sig, preimage, witness_script])
else:
revocation_pubkey = privkey_to_pubkey(privkey)
txin.make_witness = lambda sig: construct_witness([sig, revocation_pubkey, witness_script])
return txin
def sweep_their_ctx_to_remote(
ctx: Transaction, output_idx: int,
our_payment_privkey: ecc.ECPrivkey,
has_anchors: bool,
) -> Optional[PartialTxInput]:
assert has_anchors is True
our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True)
val = ctx.outputs()[output_idx].value
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
txin = PartialTxInput(prevout=prevout)
txin._trusted_value_sats = val
desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey.hex(), script_type='p2wpkh')
witness_script = make_commitment_output_to_remote_witness_script(our_payment_pubkey)
txin.script_descriptor = desc
txin.num_sig = 1
txin.script_sig = b''
txin.witness_script = witness_script
txin.nsequence = 1
txin.privkey = our_payment_privkey.get_secret_bytes()
txin.make_witness = lambda sig: construct_witness([sig, witness_script])
return txin
def sweep_ctx_anchor(*, ctx: Transaction, multisig_key: Keypair) -> Optional[PartialTxInput]:
from .lnutil import make_commitment_output_to_anchor_address, make_commitment_output_to_anchor_witness_script
local_funding_pubkey = multisig_key.pubkey
local_anchor_address = make_commitment_output_to_anchor_address(local_funding_pubkey)
witness_script = make_commitment_output_to_anchor_witness_script(local_funding_pubkey)
output_idxs = ctx.get_output_idxs_from_address(local_anchor_address)
if not output_idxs:
return
output_idx = output_idxs.pop()
val = ctx.outputs()[output_idx].value
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
txin = PartialTxInput(prevout=prevout)
txin._trusted_value_sats = val
txin.script_sig = b''
txin.witness_script = witness_script
txin.nsequence = 0xffffffff - 2
txin.privkey = multisig_key.privkey
txin.make_witness = lambda sig: construct_witness([sig, witness_script])
return txin
def sweep_ctx_to_local(
*, ctx: Transaction, output_idx: int, witness_script: bytes,
privkey: bytes, is_revocation: bool,
to_self_delay: int = None) -> Optional[PartialTxInput]:
"""Create a txin that sweeps the 'to_local' output of a commitment
transaction into our wallet.
privkey: either revocation_privkey or localdelayed_privkey
is_revocation: tells us which ^
"""
val = ctx.outputs()[output_idx].value
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
txin = PartialTxInput(prevout=prevout)
txin._trusted_value_sats = val
txin.script_sig = b''
txin.witness_script = witness_script
txin.nsequence = 0xffffffff - 2
if not is_revocation:
assert isinstance(to_self_delay, int)
txin.nsequence = to_self_delay
txin.privkey = privkey
assert txin.witness_script
txin.make_witness = lambda sig: construct_witness([sig, int(is_revocation), witness_script])
return txin
def sweep_htlctx_output(
*, htlc_tx: Transaction,
output_idx: int,
htlctx_witness_script: bytes,
privkey: bytes,
is_revocation: bool,
to_self_delay: int = None,
) -> Optional[PartialTxInput]:
"""Create a txn that sweeps the output of a first stage htlc tx
(i.e. sweeps from an HTLC-Timeout or an HTLC-Success tx).
"""
# note: this is the same as sweeping the to_local output of the ctx,
# as these are the same script (address-reuse).
return sweep_ctx_to_local(
ctx=htlc_tx,
output_idx=output_idx,
witness_script=htlctx_witness_script,
privkey=privkey,
is_revocation=is_revocation,
to_self_delay=to_self_delay,
)