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.
901 lines
37 KiB
Python
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,
|
|
)
|