diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index d04c85f79..433337d72 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -56,9 +56,10 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey ShortChannelID, map_htlcs_to_ctx_output_idxs, fee_for_htlc_output, offered_htlc_trim_threshold_sat, received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT, - ChannelType, LNProtocolWarning) + ChannelType, LNProtocolWarning, ctx_has_anchors) from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo +from .lnsweep import create_sweeptx_their_backup_ctx from .lnhtlc import HTLCManager from .lnmsg import encode_msg, decode_msg from .address_synchronizer import TX_HEIGHT_LOCAL @@ -594,14 +595,19 @@ class ChannelBackup(AbstractChannel): return True def create_sweeptxs_for_their_ctx(self, ctx): - return {} + return create_sweeptx_their_backup_ctx(chan=self, ctx=ctx, sweep_address=self.get_sweep_address()) def create_sweeptxs_for_our_ctx(self, ctx): if self.is_imported: return create_sweeptxs_for_our_ctx(chan=self, ctx=ctx, sweep_address=self.get_sweep_address()) else: - # backup from op_return - return {} + return + + def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]: + return None + + def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None: + return None def get_funding_address(self): return self.cb.funding_address diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index d76efd6ee..4b1a123c6 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -18,14 +18,15 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o 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) -from .transaction import (Transaction, TxOutput, PartialTransaction, PartialTxInput, - PartialTxOutput, TxOutpoint) + map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script, + derive_payment_basepoint, ctx_has_anchors, SCRIPT_TEMPLATE_FUNDING) +from .transaction import (Transaction, TxInput, PartialTransaction, PartialTxInput, + PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template) from .simple_config import SimpleConfig from .logging import get_logger, Logger if TYPE_CHECKING: - from .lnchannel import Channel, AbstractChannel + from .lnchannel import Channel, AbstractChannel, ChannelBackup _logger = get_logger(__name__) @@ -352,6 +353,69 @@ def extract_ctx_secrets(chan: 'Channel', ctx: Transaction): 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 create_sweeptx_their_backup_ctx( + *, chan: 'ChannelBackup', + ctx: Transaction, + sweep_address: str) -> 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 pubkey in 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 {} + + # to_remote + csv_delay = 1 + 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 + sweep_tx = lambda: create_sweeptx_their_ctx_to_remote( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + our_payment_privkey=our_payment_privkey, + config=chan.lnworker.config, + has_anchors=True + ) + txs[prevout] = SweepInfo( + name='their_ctx_to_remote_backup', + csv_delay=csv_delay, + cltv_abs=0, + gen_tx=sweep_tx) + return txs + + def create_sweeptxs_for_their_ctx( *, chan: 'Channel', ctx: Transaction, diff --git a/electrum/lnutil.py b/electrum/lnutil.py index f4e97f415..448f34bfe 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -21,7 +21,7 @@ from .util import format_short_id as format_short_channel_id from .crypto import sha256, pw_decode_with_version_and_mac from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint, - PartialTxOutput, opcodes, TxOutput) + PartialTxOutput, opcodes, TxOutput, OPPushDataPubkey) from . import bitcoin, crypto, transaction from . import descriptor from .bitcoin import (redeem_script_to_address, address_to_script, @@ -57,6 +57,8 @@ FIXED_ANCHOR_SAT = 330 LN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1 DUST_LIMIT_MAX = 1000 +SCRIPT_TEMPLATE_FUNDING = [opcodes.OP_2, OPPushDataPubkey, OPPushDataPubkey, opcodes.OP_2, opcodes.OP_CHECKMULTISIG] + from .json_db import StoredObject, stored_in, stored_as @@ -1264,6 +1266,14 @@ def extract_ctn_from_tx_and_chan(tx: Transaction, chan: 'AbstractChannel') -> in funder_payment_basepoint=funder_conf.payment_basepoint.pubkey, fundee_payment_basepoint=fundee_conf.payment_basepoint.pubkey) +def ctx_has_anchors(tx: Transaction): + output_values = [output.value for output in tx.outputs()] + if FIXED_ANCHOR_SAT in output_values: + return True + else: + return False + + class LnFeatureContexts(enum.Flag): INIT = enum.auto()