diff --git a/electrum/daemon.py b/electrum/daemon.py index c40d1f96bda5..7b7d28bb9256 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -539,6 +539,8 @@ def load_wallet(self, path, password, *, manual_upgrades=True) -> Optional[Abstr return wallet = Wallet(db, storage, config=self.config) wallet.start_network(self.network) + if wallet.lnworker: + wallet.lnworker.maybe_enable_anchors_store_password(password) self._wallets[path] = wallet return wallet diff --git a/electrum/gui/icons/anchor.png b/electrum/gui/icons/anchor.png new file mode 100644 index 000000000000..20b152fe813b Binary files /dev/null and b/electrum/gui/icons/anchor.png differ diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 97d5d6699711..08b680c86cd0 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -655,7 +655,7 @@ def on_start(self): util.register_callback(self.on_channel_db, ['channel_db']) util.register_callback(self.set_num_peers, ['gossip_peers']) util.register_callback(self.set_unknown_channels, ['unknown_channels']) - + if self.network and self.electrum_config.get('auto_connect') is None: self.popup_dialog("first_screen") # load_wallet_on_start will be called later, after initial network setup is completed @@ -690,6 +690,8 @@ def on_wizard_success(self, storage, db, password): self.logger.info(f'use single password: {self._use_single_password}') wallet = Wallet(db, storage, config=self.electrum_config) wallet.start_network(self.daemon.network) + if wallet.lnworker: + wallet.lnworker.maybe_enable_anchors_store_password(password) self.daemon.add_wallet(wallet) self.load_wallet(wallet) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 3c46a1d07bf3..97c0e83c5c87 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -353,7 +353,7 @@ def start_new_window(self, path, uri, *, app_is_starting=False) -> Optional[Elec def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) try: - path, storage = wizard.select_storage(path, self.daemon.get_wallet) + path, storage, password = wizard.select_storage(path, self.daemon.get_wallet) # storage is None if file does not exist if storage is None: wizard.path = path # needed by trustedcoin plugin @@ -372,6 +372,8 @@ def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wa if storage is None or db.get_action(): return wallet = Wallet(db, storage, config=self.config) + if wallet.lnworker: + wallet.lnworker.maybe_enable_anchors_store_password(password) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 0d423f4451a0..5f4d33d2ee8d 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -529,6 +529,13 @@ def icon(self) -> QIcon: return read_QIcon("nocloud") +class ChanFeatAnchors(ChannelFeature): + def tooltip(self) -> str: + return _("This channel uses anchor outputs.") + def icon(self) -> QIcon: + return read_QIcon("anchor") + + class ChannelFeatureIcons: ICON_SIZE = QSize(16, 16) @@ -548,6 +555,8 @@ def from_channel(cls, chan: AbstractChannel) -> 'ChannelFeatureIcons': feats.append(ChanFeatTrampoline()) if not chan.has_onchain_backup(): feats.append(ChanFeatNoOnchainBackup()) + if chan.has_anchors(): + feats.append(ChanFeatAnchors()) return ChannelFeatureIcons(feats) def paint(self, painter: QPainter, rect: QRect) -> None: diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index c20ee76f330d..eff5fb395bb1 100644 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -201,7 +201,7 @@ def __init__(self, config: 'SimpleConfig', app: QApplication, plugins: 'Plugins' self.raise_() self.refresh_gui() # Need for QT on MacOSX. Lame. - def select_storage(self, path, get_wallet_from_daemon) -> Tuple[str, Optional[WalletStorage]]: + def select_storage(self, path, get_wallet_from_daemon) -> Tuple[str, Optional[WalletStorage], Optional[str]]: vbox = QVBoxLayout() hbox = QHBoxLayout() @@ -302,8 +302,9 @@ def on_filename(filename): get_new_wallet_name(wallet_folder))) name_e.textChanged.connect(on_filename) name_e.setText(os.path.basename(path)) + password = None - def run_user_interaction_loop(): + def run_user_interaction_loop() -> Optional[str]: while True: if self.loop.exec_() != 2: # 2 = next raise UserCancelled() @@ -320,7 +321,7 @@ def run_user_interaction_loop(): password = pw_e.text() try: temp_storage.decrypt(password) - break + return password except InvalidPassword as e: self.show_message(title=_('Error'), msg=str(e)) continue @@ -351,14 +352,14 @@ def run_user_interaction_loop(): raise Exception('Unexpected encryption version') try: - run_user_interaction_loop() + password = run_user_interaction_loop() finally: try: pw_e.clear() except RuntimeError: # wrapped C/C++ object has been deleted. pass # happens when decrypting with hw device - return temp_storage.path, (temp_storage if temp_storage.file_exists() else None) + return temp_storage.path, (temp_storage if temp_storage.file_exists() else None), password def run_upgrades(self, storage: WalletStorage, db: 'WalletDB') -> None: path = storage.path diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index dd08135daa08..abd72afcf0f0 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -39,7 +39,7 @@ from .invoices import PR_PAID from .bitcoin import redeem_script_to_address from .crypto import sha256, sha256d -from .transaction import Transaction, PartialTransaction, TxInput +from .transaction import Transaction, PartialTransaction, TxInput, Sighash from .logging import Logger from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure from . import lnutil @@ -52,9 +52,11 @@ ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script, ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr, fee_for_htlc_output, offered_htlc_trim_threshold_sat, - received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address) -from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx -from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo + 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 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 from .address_synchronizer import TX_HEIGHT_LOCAL @@ -221,10 +223,10 @@ def delete_closing_height(self): self.storage.pop('closing_height', None) def create_sweeptxs_for_our_ctx(self, ctx): - return create_sweeptxs_for_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address) + return txs_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address) def create_sweeptxs_for_their_ctx(self, ctx): - return create_sweeptxs_for_their_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address) + return txs_their_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address) def is_backup(self): return False @@ -323,9 +325,11 @@ def update_closed_state(self, *, funding_txid: str, funding_height: TxMinedInfo, def sweep_address(self) -> str: # TODO: in case of unilateral close with pending HTLCs, this address will be reused addr = None - if self.is_static_remotekey_enabled(): + if self.has_anchors(): + addr = self.lnworker.wallet.get_new_sweep_address_for_channel() + elif self.is_static_remotekey_enabled(): our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey - addr = make_commitment_output_to_remote_address(our_payment_pubkey) + addr = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=False) if addr is None: addr = self._fallback_sweep_address assert addr @@ -399,6 +403,10 @@ def is_frozen_for_receiving(self) -> bool: def is_static_remotekey_enabled(self) -> bool: pass + @abstractmethod + def has_anchors(self) -> bool: + pass + @abstractmethod def get_local_pubkey(self) -> bytes: """Returns our node ID.""" @@ -438,8 +446,13 @@ def init_config(self, cb): self.config[LOCAL] = LocalConfig.from_seed( channel_seed=cb.channel_seed, to_self_delay=cb.local_delay, + # there are three cases of backups: + # 1. legacy: payment_basepoint will be derived + # 2. static_remotekey: to_remote sweep not necessary due to wallet address + # 3. anchor outputs: sweep to_remote by deriving the key from the funding pubkeys # dummy values static_remotekey=None, + static_payment_key=None, dust_limit_sat=None, max_htlc_value_in_flight_msat=None, max_accepted_htlcs=None, @@ -481,14 +494,19 @@ def is_backup(self): return True def create_sweeptxs_for_their_ctx(self, ctx): - return {} + return tx_their_ctx_to_remote_backup(chan=self, ctx=ctx, sweep_address=self.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.sweep_address) + return txs_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address) else: - # backup from op_return - return {} + return + + 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 def get_funding_address(self): return self.cb.funding_address @@ -526,6 +544,9 @@ def is_static_remotekey_enabled(self) -> bool: # their local config is not static) return False + def has_anchors(self) -> Optional[bool]: + return None + def get_local_pubkey(self) -> bytes: cb = self.cb assert isinstance(cb, ChannelBackupStorage) @@ -711,11 +732,14 @@ def construct_channel_announcement_without_sigs(self) -> bytes: def is_static_remotekey_enabled(self) -> bool: return bool(self.storage.get('static_remotekey_enabled')) + def has_anchors(self) -> bool: + return bool(self.storage.get('has_anchors')) + def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]: ret = [] if self.is_static_remotekey_enabled(): our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey - to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey) + to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors()) ret.append(to_remote_address) return ret @@ -951,6 +975,10 @@ def sign_next_commitment(self) -> Tuple[bytes, Sequence[bytes]]: commit=pending_remote_commitment, ctx_output_idx=ctx_output_idx, htlc=htlc) + if self.has_anchors(): + # we send a signature with the following sighash flags + # for the peer to be able to replace inputs and outputs + htlc_tx.inputs()[0].sighash = Sighash.ANYONECANPAY | Sighash.SINGLE sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey)) htlc_sig = ecc.sig_string_from_der_sig(sig[:-1]) htlcsigs.append((ctx_output_idx, htlc_sig)) @@ -1012,6 +1040,9 @@ def _verify_htlc_sig(self, *, htlc: UpdateAddHtlc, htlc_sig: bytes, htlc_directi commit=ctx, ctx_output_idx=ctx_output_idx, htlc=htlc) + if self.has_anchors(): + # peer sent us a signature for our ctx using anchor sighash flags + htlc_tx.inputs()[0].sighash = Sighash.ANYONECANPAY | Sighash.SINGLE pre_hash = sha256d(bfh(htlc_tx.serialize_preimage(0))) remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, pcp) if not ecc.verify_signature(remote_htlc_pubkey, htlc_sig, pre_hash): @@ -1021,7 +1052,8 @@ def get_remote_htlc_sig_for_htlc(self, *, htlc_relative_idx: int) -> bytes: data = self.config[LOCAL].current_htlc_signatures htlc_sigs = list(chunks(data, 64)) htlc_sig = htlc_sigs[htlc_relative_idx] - remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sig) + b'\x01' + remote_sighash = Sighash.ALL if not self.has_anchors() else Sighash.ANYONECANPAY | Sighash.SINGLE + remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sig) + remote_sighash.to_bytes(1, 'big') return remote_htlc_sig def revoke_current_commitment(self): @@ -1142,7 +1174,7 @@ def balance_tied_up_in_htlcs_by_direction(self, ctx_owner: HTLCOwner = LOCAL, *, return htlcsum(self.hm.htlcs_by_direction(ctx_owner, direction, ctn).values()) def available_to_spend(self, subject: HTLCOwner, *, strict: bool = True) -> int: - """The usable balance of 'subject' in msat, after taking reserve and fees into + """The usable balance of 'subject' in msat, after taking reserve and fees (and anchors) into consideration. Note that fees (and hence the result) fluctuate even without user interaction. """ assert type(subject) is HTLCOwner @@ -1163,28 +1195,47 @@ def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int: feerate=feerate, is_local_initiator=self.constraints.is_initiator, round_to_sat=False, + has_anchors=self.has_anchors() ) htlc_fee_msat = fee_for_htlc_output(feerate=feerate) htlc_trim_func = received_htlc_trim_threshold_sat if ctx_owner == receiver else offered_htlc_trim_threshold_sat - htlc_trim_threshold_msat = htlc_trim_func(dust_limit_sat=self.config[ctx_owner].dust_limit_sat, feerate=feerate) * 1000 - if sender == initiator == LOCAL: # see https://github.com/lightningnetwork/lightning-rfc/pull/740 + htlc_trim_threshold_msat = htlc_trim_func(dust_limit_sat=self.config[ctx_owner].dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors()) * 1000 + + # the sender cannot spend below its reserve + max_send_msat = sender_balance_msat - sender_reserve_msat + + # reserve a fee spike buffer + # see https://github.com/lightningnetwork/lightning-rfc/pull/740 + if sender == initiator == LOCAL: fee_spike_buffer = calc_fees_for_commitment_tx( num_htlcs=num_htlcs_in_ctx + int(not is_htlc_dust) + 1, feerate=2 * feerate, is_local_initiator=self.constraints.is_initiator, round_to_sat=False, - )[sender] - max_send_msat = sender_balance_msat - sender_reserve_msat - fee_spike_buffer - else: - max_send_msat = sender_balance_msat - sender_reserve_msat - ctx_fees_msat[sender] + has_anchors=self.has_anchors())[sender] + max_send_msat -= fee_spike_buffer + # we can't enforce the fee spike buffer on the remote party + elif sender == initiator == REMOTE: + max_send_msat -= ctx_fees_msat[sender] + + # initiator pays for anchor outputs + if sender == initiator and self.has_anchors(): + max_send_msat -= 2 * FIXED_ANCHOR_SAT + + # handle the transaction fees for the HTLC transaction if is_htlc_dust: + # nobody pays additional HTLC transaction fees return min(max_send_msat, htlc_trim_threshold_msat - 1) else: + # somebody has to pay for the additonal HTLC transaction fees if sender == initiator: return max_send_msat - htlc_fee_msat else: - # the receiver is the initiator, so they need to be able to pay tx fees - if receiver_balance_msat - receiver_reserve_msat - ctx_fees_msat[receiver] - htlc_fee_msat < 0: + # check if the receiver can afford to pay for the HTLC transaction fees + new_receiver_balance = receiver_balance_msat - receiver_reserve_msat - ctx_fees_msat[receiver] - htlc_fee_msat + if self.has_anchors(): + new_receiver_balance -= 2 * FIXED_ANCHOR_SAT + if new_receiver_balance < 0: return 0 return max_send_msat @@ -1203,7 +1254,7 @@ def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int: def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = None, *, - feerate: int = None) -> Sequence[UpdateAddHtlc]: + feerate: int = None) -> List[UpdateAddHtlc]: """Returns list of non-dust HTLCs for subject's commitment tx at ctn, filtered by direction (of HTLCs). """ @@ -1215,9 +1266,9 @@ def included_htlcs(self, subject: HTLCOwner, direction: Direction, ctn: int = No feerate = self.get_feerate(subject, ctn=ctn) conf = self.config[subject] if direction == RECEIVED: - threshold_sat = received_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate) + threshold_sat = received_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors()) else: - threshold_sat = offered_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate) + threshold_sat = offered_htlc_trim_threshold_sat(dust_limit_sat=conf.dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors()) htlcs = self.hm.htlcs_by_direction(subject, direction, ctn=ctn).values() return list(filter(lambda htlc: htlc.amount_msat // 1000 >= threshold_sat, htlcs)) @@ -1265,9 +1316,9 @@ def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> PartialTransact return self.get_commitment(subject, ctn=ctn) def create_sweeptxs(self, ctn: int) -> List[Transaction]: - from .lnsweep import create_sweeptxs_for_watchtower + from .lnsweep import txs_their_ctx_watchtower secret, ctx = self.get_secret_and_commitment(REMOTE, ctn=ctn) - return create_sweeptxs_for_watchtower(self, ctx, secret, self.sweep_address) + return txs_their_ctx_watchtower(self, ctx, secret, self.sweep_address) def get_oldest_unrevoked_ctn(self, subject: HTLCOwner) -> int: return self.hm.ctn_oldest_unrevoked(subject) @@ -1360,6 +1411,7 @@ def update_fee(self, feerate: int, from_us: bool) -> None: num_htlcs=num_htlcs_in_ctx, feerate=feerate, is_local_initiator=self.constraints.is_initiator, + has_anchors=self.has_anchors() ) remainder = sender_balance_msat - sender_reserve_msat - ctx_fees_msat[sender] if remainder < 0: @@ -1402,13 +1454,15 @@ def make_commitment(self, subject: HTLCOwner, this_point: bytes, ctn: int) -> Pa remote_htlc_pubkey=other_htlc_pubkey, local_htlc_pubkey=this_htlc_pubkey, payment_hash=htlc.payment_hash, - cltv_expiry=htlc.cltv_expiry), htlc)) + cltv_expiry=htlc.cltv_expiry, + has_anchors=self.has_anchors()), htlc)) # note: maybe flip initiator here for fee purposes, we want LOCAL and REMOTE # in the resulting dict to correspond to the to_local and to_remote *outputs* of the ctx onchain_fees = calc_fees_for_commitment_tx( num_htlcs=len(htlcs), feerate=feerate, is_local_initiator=self.constraints.is_initiator == (subject == LOCAL), + has_anchors=self.has_anchors(), ) if self.is_static_remotekey_enabled(): @@ -1434,22 +1488,27 @@ def make_commitment(self, subject: HTLCOwner, this_point: bytes, ctn: int) -> Pa dust_limit_sat=this_config.dust_limit_sat, fees_per_participant=onchain_fees, htlcs=htlcs, + has_anchors=self.has_anchors() ) def make_closing_tx(self, local_script: bytes, remote_script: bytes, fee_sat: int, *, drop_remote = False) -> Tuple[bytes, PartialTransaction]: """ cooperative close """ _, outputs = make_commitment_outputs( - fees_per_participant={ - LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0, - REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0, - }, - local_amount_msat=self.balance(LOCAL), - remote_amount_msat=self.balance(REMOTE) if not drop_remote else 0, - local_script=bh2u(local_script), - remote_script=bh2u(remote_script), - htlcs=[], - dust_limit_sat=self.config[LOCAL].dust_limit_sat) + fees_per_participant={ + LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0, + REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0, + }, + local_amount_msat=self.balance(LOCAL), + remote_amount_msat=self.balance(REMOTE) if not drop_remote else 0, + local_script=bh2u(local_script), + remote_script=bh2u(remote_script), + htlcs=[], + dust_limit_sat=self.config[LOCAL].dust_limit_sat, + has_anchors=self.has_anchors(), + local_anchor_script=None, + remote_anchor_script=None, + ) closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey, self.config[REMOTE].multisig_key.pubkey, @@ -1482,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 create_sweeptx_for_their_revoked_htlc(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) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 1435389dc861..1def2467fb8f 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -511,6 +511,9 @@ def is_static_remotekey(self): def is_upfront_shutdown_script(self): return self.features.supports(LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT) + def use_anchors(self) -> bool: + return self.features.supports(LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT) + def upfront_shutdown_script_from_payload(self, payload, msg_identifier: str) -> Optional[bytes]: if msg_identifier not in ['accept', 'open']: raise ValueError("msg_identifier must be either 'accept' or 'open'") @@ -534,12 +537,17 @@ def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwn # flexibility to decide an address at closing time upfront_shutdown_script = b'' - if self.is_static_remotekey(): - wallet = self.lnworker.wallet - assert wallet.txin_type == 'p2wpkh' - addr = wallet.get_new_sweep_address_for_channel() - static_remotekey = bfh(wallet.get_public_key(addr)) + if self.use_anchors(): + static_payment_key = self.lnworker.static_payment_key + static_remotekey = None + elif self.is_static_remotekey(): + wallet = self.lnworker.wallet + assert wallet.txin_type == 'p2wpkh' + addr = wallet.get_new_sweep_address_for_channel() + static_payment_key = None + static_remotekey = bfh(wallet.get_public_key(addr)) else: + static_payment_key = None static_remotekey = None dust_limit_sat = bitcoin.DUST_LIMIT_P2PKH reserve_sat = max(funding_sat // 100, dust_limit_sat) @@ -550,6 +558,7 @@ def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwn local_config = LocalConfig.from_seed( channel_seed=channel_seed, static_remotekey=static_remotekey, + static_payment_key=static_payment_key, upfront_shutdown_script=upfront_shutdown_script, to_self_delay=self.network.config.get('lightning_to_self_delay', 7 * 144), dust_limit_sat=dust_limit_sat, @@ -680,6 +689,7 @@ async def channel_establishment_flow( funding_sat=funding_sat, is_local_initiator=True, initial_feerate_per_kw=feerate, + has_anchors=self.use_anchors(), ) # -> funding created @@ -770,6 +780,7 @@ def create_channel_storage(self, channel_id, outpoint, local_config, remote_conf "unfulfilled_htlcs": {}, # htlc_id -> error_bytes, failure_message "revocation_store": {}, "static_remotekey_enabled": self.is_static_remotekey(), # stored because it cannot be "downgraded", per BOLT2 + "has_anchors": self.use_anchors(), } return StoredDict(chan_dict, self.lnworker.db if self.lnworker else None, []) @@ -821,6 +832,7 @@ async def on_open_channel(self, payload): funding_sat=funding_sat, is_local_initiator=False, initial_feerate_per_kw=feerate, + has_anchors=self.use_anchors(), ) # note: we ignore payload['channel_flags'], which e.g. contains 'announce_channel'. diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 7d24d3de0387..31daeab3b239 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -5,8 +5,9 @@ from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Callable from enum import Enum, auto -from .util import bfh, bh2u +from .util import bfh, bh2u, UneconomicFee from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness +from . import coinchooser from . import ecc 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, @@ -14,18 +15,23 @@ LOCAL, REMOTE, make_htlc_output_witness_script, get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed, RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED, - map_htlcs_to_ctx_output_idxs, Direction) -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__) +HTLC_TRANSACTION_DEADLINE_FRACTION = 4 +HTLC_TRANSACTION_SWEEP_TARGET = 10 +HTLCTX_INPUT_OUTPUT_INDEX = 0 + class SweepInfo(NamedTuple): name: str @@ -34,158 +40,64 @@ class SweepInfo(NamedTuple): gen_tx: Callable[[], Optional[Transaction]] -def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes, - sweep_address: str) -> List[Transaction]: - """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 +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) - 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) - txs = [] - # to_local - revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True) - witness_script = bh2u(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_tx = create_sweeptx_ctx_to_local( - sweep_address=sweep_address, - ctx=ctx, - output_idx=output_idx, - witness_script=bfh(witness_script), - privkey=other_revocation_privkey, - is_revocation=True, - config=chan.lnworker.config) - if sweep_tx: - txs.append(sweep_tx) - # HTLCs - def create_sweeptx_for_htlc(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction, - ctx_output_idx: int) -> Optional[Transaction]: - 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 create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( - htlc_tx=htlc_tx, - 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, - 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 = create_sweeptx_for_htlc( - htlc=htlc, - htlc_direction=direction, - ctx_output_idx=ctx_output_idx) - if secondstage_sweep_tx: - txs.append(secondstage_sweep_tx) - return txs + 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 + #_logger.info(f'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 create_sweeptx_for_their_revoked_ctx( - chan: 'Channel', - ctx: Transaction, - per_commitment_secret: bytes, - sweep_address: str) -> Optional[Callable[[], Optional[Transaction]]]: - # 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) - txs = [] - # to_local - revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True) - witness_script = bh2u(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_tx = lambda: create_sweeptx_ctx_to_local( - sweep_address=sweep_address, - ctx=ctx, - output_idx=output_idx, - witness_script=bfh(witness_script), - privkey=other_revocation_privkey, - is_revocation=True, - config=chan.lnworker.config) - return sweep_tx - return None +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_for_their_revoked_htlc( - chan: 'Channel', - ctx: Transaction, - htlc_tx: Transaction, - sweep_address: str) -> Optional[SweepInfo]: - x = analyze_ctx(chan, ctx) - if not x: - return - ctn, their_pcp, is_revocation, per_commitment_secret = x - if not is_revocation: - return - # 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) - # same witness script as to_local - revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True) - 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: create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( - 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) - - -def create_sweeptxs_for_our_ctx( +def txs_our_ctx( *, chan: 'AbstractChannel', ctx: Transaction, sweep_address: str) -> Optional[Dict[str, SweepInfo]]: - """Handle the case where we force close unilaterally with our latest ctx. - Construct sweep txns for 'to_local', and for all HTLCs (2 txns each). + """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) @@ -206,7 +118,7 @@ def create_sweeptxs_for_our_ctx( # to remote address bpk = their_conf.payment_basepoint.pubkey their_payment_pubkey = bpk if chan.is_static_remotekey_enabled() else derive_pubkey(their_conf.payment_basepoint.pubkey, our_pcp) - to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey) + to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey, has_anchors=chan.has_anchors()) # test ctx _logger.debug(f'testing our ctx: {to_local_address} {to_remote_address}') if not ctx.get_output_idxs_from_address(to_local_address) \ @@ -221,7 +133,7 @@ def create_sweeptxs_for_our_ctx( output_idxs = ctx.get_output_idxs_from_address(to_local_address) if output_idxs: output_idx = output_idxs.pop() - sweep_tx = lambda: create_sweeptx_ctx_to_local( + sweep_tx = lambda: tx_ctx_to_local( sweep_address=sweep_address, ctx=ctx, output_idx=output_idx, @@ -243,14 +155,14 @@ def create_sweeptxs_for_our_ctx( return txs # HTLCs - def create_txns_for_htlc( + def txs_htlc( *, htlc: 'UpdateAddHtlc', htlc_direction: Direction, ctx_output_idx: int, htlc_relative_idx: int): if htlc_direction == RECEIVED: preimage = chan.lnworker.get_preimage(htlc.payment_hash) else: preimage = None - htlctx_witness_script, htlc_tx = create_htlctx_that_spends_from_our_ctx( + htlctx_witness_script, htlc_tx = tx_our_ctx_htlctx( chan=chan, our_pcp=our_pcp, ctx=ctx, @@ -260,21 +172,25 @@ def create_txns_for_htlc( htlc_direction=htlc_direction, ctx_output_idx=ctx_output_idx, htlc_relative_idx=htlc_relative_idx) - sweep_tx = lambda: create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( + # 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, @@ -289,75 +205,214 @@ def create_txns_for_htlc( subject=LOCAL, ctn=ctn) for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items(): - create_txns_for_htlc( - htlc=htlc, - htlc_direction=direction, - ctx_output_idx=ctx_output_idx, - htlc_relative_idx=htlc_relative_idx) + try: + txs_htlc( + htlc=htlc, + htlc_direction=direction, + ctx_output_idx=ctx_output_idx, + htlc_relative_idx=htlc_relative_idx) + except UneconomicFee: + continue return txs -def analyze_ctx(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) +def tx_ctx_to_local( + *, sweep_address: str, ctx: Transaction, output_idx: int, witness_script: bytes, + privkey: bytes, is_revocation: bool, config: SimpleConfig, + to_self_delay: int = None) -> Optional[PartialTransaction]: + """Create a txn 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 + sweep_inputs = [txin] + if not is_revocation: + assert isinstance(to_self_delay, int) + sweep_inputs[0].nsequence = to_self_delay + tx_size_bytes = 121 # approx size of to_local -> p2wpkh + fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) + outvalue = val - fee + if outvalue <= dust_threshold(): + return None + sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] + sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2) + sig = sweep_tx.sign_txin(0, privkey) + witness = construct_witness([sig, int(is_revocation), witness_script]) + sweep_tx.inputs()[0].witness = bfh(witness) + return sweep_tx + + +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) - 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 - #_logger.info(f'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 + witness_script, 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_{bh2u(htlc.payment_hash)}') + + # we need to attach inputs that pay for the transaction fee + if chan.has_anchors(): + wallet = chan.lnworker.wallet + coins = wallet.get_spendable_coins(None) + + def fee_estimator(size): + if htlc_direction == SENT: + # we deal with an offered HTLC and therefore with a timeout transaction + # in this case it is not time critical for us to sweep unless we + # become a forwarding node + fee_per_kb = wallet.config.eta_target_to_fee(HTLC_TRANSACTION_SWEEP_TARGET) + else: + # in the case of a received HTLC, if we have the hash preimage, + # we should sweep before the timelock expires + expiry_height = htlc.cltv_expiry + current_height = wallet.network.blockchain().height() + deadline_blocks = expiry_height - current_height + # target block inclusion with a safety buffer + target = int(deadline_blocks / HTLC_TRANSACTION_DEADLINE_FRACTION) + fee_per_kb = wallet.config.eta_target_to_fee(target) + if not fee_per_kb: # testnet and other cases + fee_per_kb = wallet.config.fee_per_kb() + fee = wallet.config.estimate_fee_for_feerate(fee_per_kb=fee_per_kb, size=size) + # we only sweep if it is makes sense economically + if fee > htlc.amount_msat // 1000: + raise UneconomicFee + return fee + + coin_chooser = coinchooser.get_coin_chooser(wallet.config) + change_address = wallet.get_single_change_address_for_new_transaction() + funded_htlc_tx = coin_chooser.make_tx( + coins=coins, + inputs=maybe_zero_fee_htlc_tx.inputs(), + outputs=maybe_zero_fee_htlc_tx.outputs(), + change_addrs=[change_address], + fee_estimator_vb=fee_estimator, + dust_threshold=wallet.dust_threshold()) + + # place htlc input/output at corresponding indices (due to sighash single) + 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()[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 != HTLCTX_INPUT_OUTPUT_INDEX: + htlc_txin = inputs.pop(htlc_input_idx) + 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(HTLCTX_INPUT_OUTPUT_INDEX, htlc_txout) + final_htlc_tx = PartialTransaction.from_io( + inputs, + outputs, + locktime=maybe_zero_fee_htlc_tx.locktime, + version=maybe_zero_fee_htlc_tx.version, + BIP69_sort=False + ) + + for fee_input_idx in range(1, len(funded_htlc_tx.inputs())): + txin = final_htlc_tx.inputs()[fee_input_idx] + pubkey = wallet.get_public_key(txin.address) + index = wallet.get_address_index(txin.address) + privkey, _ = wallet.keystore.get_private_key(index, chan.lnworker.wallet_password) + txin.num_sig = 1 + txin.script_type = 'p2wpkh' + txin.pubkeys = [bfh(pubkey)] + fee_input_sig = final_htlc_tx.sign_txin(fee_input_idx, privkey) + final_htlc_tx.add_signature_to_txin(txin_idx=fee_input_idx, signing_pubkey=pubkey, sig=fee_input_sig) else: - return - return ctn, their_pcp, is_revocation, per_commitment_secret + final_htlc_tx = maybe_zero_fee_htlc_tx + + # 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(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 create_sweeptxs_for_their_ctx( +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 + (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 tx_ctx_to_local( + sweep_address=sweep_address, + ctx=htlc_tx, + output_idx=output_idx, + witness_script=htlctx_witness_script, + privkey=privkey, + is_revocation=is_revocation, + to_self_delay=to_self_delay, + config=config, + ) + + +def txs_their_ctx( *, chan: 'Channel', ctx: Transaction, sweep_address: str) -> 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. + """Handle the case where the remote force-closes with their ctx. + + 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 = analyze_ctx(chan, ctx) + x = extract_ctx_secrets(chan, ctx) if not x: return ctn, their_pcp, is_revocation, per_commitment_secret = x - # to_local and to_remote addresses + # 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 = bh2u(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 + # to_remote bpk = our_conf.payment_basepoint.pubkey our_payment_pubkey = bpk if chan.is_static_remotekey_enabled() else derive_pubkey(bpk, their_pcp) - to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey) + to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=chan.has_anchors()) # test if this is their ctx _logger.debug(f'testing their ctx: {to_local_address} {to_remote_address}') if not ctx.get_output_idxs_from_address(to_local_address) \ and not ctx.get_output_idxs_from_address(to_remote_address): return + + # to_local is handled by lnwatcher if is_revocation: our_revocation_privkey = derive_blinded_privkey(our_conf.revocation_basepoint.privkey, per_commitment_secret) - gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, chan.sweep_address) + gen_tx = tx_their_ctx_justice(chan, ctx, per_commitment_secret, chan.sweep_address) if gen_tx: tx = gen_tx() txs[tx.inputs()[0].prevout.to_str()] = SweepInfo( @@ -365,34 +420,47 @@ def create_sweeptxs_for_their_ctx( csv_delay=0, cltv_expiry=0, gen_tx=gen_tx) - # prep - 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) - # to_local is handled by lnwatcher + # to_remote - if not chan.is_static_remotekey_enabled(): + csv_delay = 0 + if chan.has_anchors(): + csv_delay = 1 + sweep_to_remote = True + our_payment_privkey = ecc.ECPrivkey(our_conf.payment_basepoint.privkey) + elif chan.is_static_remotekey_enabled(): + sweep_to_remote = False + our_payment_privkey = None + else: our_payment_bp_privkey = ecc.ECPrivkey(our_conf.payment_basepoint.privkey) our_payment_privkey = derive_privkey(our_payment_bp_privkey.secret_scalar, their_pcp) our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey) + sweep_to_remote = True + + 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 - sweep_tx = lambda: create_sweeptx_their_ctx_to_remote( + prevout = ctx.txid() + ':%d' % output_idx + sweep_tx = lambda: tx_their_ctx_to_remote( sweep_address=sweep_address, ctx=ctx, output_idx=output_idx, our_payment_privkey=our_payment_privkey, - config=chan.lnworker.config) + config=chan.lnworker.config, + has_anchors=chan.has_anchors() + ) txs[prevout] = SweepInfo( name='their_ctx_to_remote', - csv_delay=0, + csv_delay=csv_delay, cltv_expiry=0, gen_tx=sweep_tx) + # HTLCs - def create_sweeptx_for_htlc( + 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) -> None: if not is_received_htlc and not is_revocation: @@ -405,11 +473,14 @@ def create_sweeptx_for_htlc( remote_htlc_pubkey=our_htlc_privkey.get_public_key_bytes(compressed=True), local_htlc_pubkey=their_htlc_pubkey, payment_hash=htlc.payment_hash, - cltv_expiry=htlc.cltv_expiry) + cltv_expiry=htlc.cltv_expiry, + has_anchors=chan.has_anchors() + ) - cltv_expiry = htlc.cltv_expiry if is_received_htlc and not is_revocation else 0 + cltv_expiry = htlc.cltv_expiry if is_received_htlc else 0 + csv_delay = 1 if chan.has_anchors() else 0 prevout = ctx.txid() + ':%d'%ctx_output_idx - sweep_tx = lambda: create_sweeptx_their_ctx_htlc( + sweep_tx = lambda: tx_their_ctx_htlc( ctx=ctx, witness_script=htlc_output_witness_script, sweep_address=sweep_address, @@ -418,10 +489,12 @@ def create_sweeptx_for_htlc( privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(), is_revocation=is_revocation, cltv_expiry=cltv_expiry, - config=chan.lnworker.config) + config=chan.lnworker.config, + has_anchors=chan.has_anchors() + ) txs[prevout] = SweepInfo( - name=f'their_ctx_htlc_{ctx_output_idx}', - csv_delay=0, + name=f'their_ctx_htlc_{ctx_output_idx}{"_for_revoked_ctx" if is_revocation else ""}', + csv_delay=csv_delay, cltv_expiry=cltv_expiry, gen_tx=sweep_tx) # received HTLCs, in their ctx --> "timeout" @@ -433,45 +506,237 @@ def create_sweeptx_for_htlc( subject=REMOTE, ctn=ctn) for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items(): - create_sweeptx_for_htlc( + tx_htlc( htlc=htlc, is_received_htlc=direction == RECEIVED, ctx_output_idx=ctx_output_idx) return txs -def create_htlctx_that_spends_from_our_ctx( - 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'' +def txs_their_ctx_watchtower(chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes, + sweep_address: str) -> List[Transaction]: + """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) - witness_script, htlc_tx = make_htlc_tx_with_open_channel( + 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) + txs = [] + + # create justice tx for breacher's to_local output + revocation_pubkey = ecc.ECPrivkey(watcher_revocation_privkey).get_public_key_bytes(compressed=True) + witness_script = bh2u(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() + sweep_tx = tx_ctx_to_local( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + witness_script=bfh(witness_script), + privkey=watcher_revocation_privkey, + is_revocation=True, + config=chan.lnworker.config) + if sweep_tx: + txs.append(sweep_tx) + + # 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 tx_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_expiry=htlc.cltv_expiry, + has_anchors=chan.has_anchors() + ) + + cltv_expiry = htlc.cltv_expiry if is_received_htlc else 0 + return tx_their_ctx_htlc( + ctx=ctx, + witness_script=htlc_output_witness_script, + sweep_address=sweep_address, + preimage=None, + output_idx=ctx_output_idx, + privkey=watcher_revocation_privkey, + is_revocation=True, + cltv_expiry=cltv_expiry, + config=chan.lnworker.config, + has_anchors=chan.has_anchors() + ) + htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs( 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_{bh2u(htlc.payment_hash)}') - remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc_relative_idx=htlc_relative_idx) - local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey)) - txin = htlc_tx.inputs()[0] - 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, htlc_tx + 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(): + txs.append( + tx_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 txs + + # create justice transactions for HTLC transaction's outputs + def txs_their_htlctx_justice( + *, + htlc: 'UpdateAddHtlc', + htlc_direction: Direction, + ctx_output_idx: int + ) -> Optional[Transaction]: + 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 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=watcher_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, + 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 = txs_their_htlctx_justice( + htlc=htlc, + htlc_direction=direction, + ctx_output_idx=ctx_output_idx) + if secondstage_sweep_tx: + txs.append(secondstage_sweep_tx) + return txs + + +def tx_their_ctx_to_remote( + sweep_address: str, ctx: Transaction, output_idx: int, + our_payment_privkey: ecc.ECPrivkey, + config: SimpleConfig, + has_anchors: bool +) -> Optional[PartialTransaction]: + our_payment_pubkey = our_payment_privkey.get_public_key_hex(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 + txin.pubkeys = [bfh(our_payment_pubkey)] + txin.num_sig = 1 + if not has_anchors: + txin.script_type = 'p2wpkh' + tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh + else: + txin.script_sig = b'' + txin.witness_script = make_commitment_output_to_remote_witness_script(bfh(our_payment_pubkey)) + txin.nsequence = 1 + tx_size_bytes = 196 # approx size of p2wsh->p2wpkh + sweep_inputs = [txin] + fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) + outvalue = val - fee + if outvalue <= dust_threshold(): return None + sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] + sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs) + + if not has_anchors: + sweep_tx.set_rbf(True) + sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)}) + else: + sig = sweep_tx.sign_txin(0, our_payment_privkey.get_secret_bytes()) + witness = construct_witness([sig, sweep_tx.inputs()[0].witness_script]) + sweep_tx.inputs()[0].witness = bfh(witness) + + if not sweep_tx.is_complete(): + raise Exception('channel close sweep tx is not complete') + return sweep_tx + + +def tx_their_ctx_to_remote_backup( + *, 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: tx_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_expiry=0, + gen_tx=sweep_tx) + return txs -def create_sweeptx_their_ctx_htlc( + +def tx_their_ctx_htlc( ctx: Transaction, witness_script: bytes, sweep_address: str, preimage: Optional[bytes], output_idx: int, privkey: bytes, is_revocation: bool, - cltv_expiry: int, config: SimpleConfig) -> Optional[PartialTransaction]: + 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 @@ -480,6 +745,8 @@ def create_sweeptx_their_ctx_htlc( txin._trusted_value_sats = val txin.witness_script = witness_script txin.script_sig = b'' + if has_anchors: + txin.nsequence = 1 sweep_inputs = [txin] tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation) fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) @@ -498,81 +765,94 @@ def create_sweeptx_their_ctx_htlc( return tx -def create_sweeptx_their_ctx_to_remote( - sweep_address: str, ctx: Transaction, output_idx: int, - our_payment_privkey: ecc.ECPrivkey, - config: SimpleConfig) -> Optional[PartialTransaction]: - our_payment_pubkey = our_payment_privkey.get_public_key_hex(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 - txin.script_type = 'p2wpkh' - txin.pubkeys = [bfh(our_payment_pubkey)] - txin.num_sig = 1 - sweep_inputs = [txin] - tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh - fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) - outvalue = val - fee - if outvalue <= dust_threshold(): return None - sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] - sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs) - sweep_tx.set_rbf(True) - sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)}) - if not sweep_tx.is_complete(): - raise Exception('channel close sweep tx is not complete') - return sweep_tx +def tx_their_ctx_justice( + chan: 'Channel', + ctx: Transaction, + per_commitment_secret: bytes, + sweep_address: str) -> Optional[Callable[[], Optional[Transaction]]]: + # 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) + txs = [] + # to_local + revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True) + witness_script = bh2u(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_tx = lambda: tx_ctx_to_local( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + witness_script=bfh(witness_script), + privkey=other_revocation_privkey, + is_revocation=True, + config=chan.lnworker.config) + return sweep_tx + return None -def create_sweeptx_ctx_to_local( - *, sweep_address: str, ctx: Transaction, output_idx: int, witness_script: bytes, - privkey: bytes, is_revocation: bool, config: SimpleConfig, - to_self_delay: int = None) -> Optional[PartialTransaction]: - """Create a txn that sweeps the 'to_local' output of a commitment - transaction into our wallet. +def txs_their_htlctx_justice( + chan: 'Channel', + ctx: Transaction, + htlc_tx: Transaction, + sweep_address: str) -> Dict[int, SweepInfo]: + """Creates justice transactions for every output in the HTLC transaction. - privkey: either revocation_privkey or localdelayed_privkey - is_revocation: tells us which ^ + Due to anchor type channels it can happen that a remote party batches HTLC transactions, + which is why this method can return multiple SweepInfos. """ - 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 - sweep_inputs = [txin] + x = extract_ctx_secrets(chan, ctx) + if not x: + return {} + ctn, their_pcp, is_revocation, per_commitment_secret = x if not is_revocation: - assert isinstance(to_self_delay, int) - sweep_inputs[0].nsequence = to_self_delay - tx_size_bytes = 121 # approx size of to_local -> p2wpkh - fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) - outvalue = val - fee - if outvalue <= dust_threshold(): - return None - sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] - sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2) - sig = sweep_tx.sign_txin(0, privkey) - witness = construct_witness([sig, int(is_revocation), witness_script]) - sweep_tx.inputs()[0].witness = bfh(witness) - return sweep_tx + 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) -def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( - *, htlc_tx: Transaction, 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 second 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 create_sweeptx_ctx_to_local( - sweep_address=sweep_address, - ctx=htlc_tx, - output_idx=0, - witness_script=htlctx_witness_script, - privkey=privkey, - is_revocation=is_revocation, - to_self_delay=to_self_delay, - config=config, - ) + 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 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 diff --git a/electrum/lnutil.py b/electrum/lnutil.py index c22e3815bbc4..768405ba27f4 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -16,7 +16,7 @@ from .util import list_enabled_bits from .crypto import sha256 from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint, - PartialTxOutput, opcodes, TxOutput) + PartialTxOutput, opcodes, TxOutput, OPPushDataPubkey) from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number from . import ecc, bitcoin, crypto, transaction from .bitcoin import (push_script, redeem_script_to_address, address_to_script, @@ -35,13 +35,19 @@ # defined in BOLT-03: HTLC_TIMEOUT_WEIGHT = 663 +HTLC_TIMEOUT_WEIGHT_ANCHORS = 666 HTLC_SUCCESS_WEIGHT = 703 +HTLC_SUCCESS_WEIGHT_ANCHORS = 706 COMMITMENT_TX_WEIGHT = 724 +COMMITMENT_TX_WEIGHT_ANCHORS = 1124 HTLC_OUTPUT_WEIGHT = 172 +FIXED_ANCHOR_SAT = 330 LN_MAX_FUNDING_SAT = pow(2, 24) - 1 DUST_LIMIT_MAX = 1000 +SCRIPT_TEMPLATE_FUNDING = [opcodes.OP_2, OPPushDataPubkey, OPPushDataPubkey, opcodes.OP_2, opcodes.OP_CHECKMULTISIG] + # dummy address for fee estimation of funding tx def ln_dummy_address(): return redeem_script_to_address('p2wsh', '') @@ -133,6 +139,7 @@ def cross_validate_params( funding_sat: int, is_local_initiator: bool, # whether we are the funder initial_feerate_per_kw: int, + has_anchors: bool, ) -> None: # first we validate the configs separately local_config.validate_params(funding_sat=funding_sat) @@ -158,7 +165,9 @@ def cross_validate_params( if funder_config.initial_msat < calc_fees_for_commitment_tx( num_htlcs=0, feerate=initial_feerate_per_kw, - is_local_initiator=is_local_initiator)[funder]: + is_local_initiator=is_local_initiator, + has_anchors=has_anchors, + )[funder]: raise Exception( "the funder's amount for the initial commitment transaction " "is not sufficient for full fee payment") @@ -187,7 +196,6 @@ class LocalConfig(ChannelConfig): @classmethod def from_seed(self, **kwargs): channel_seed = kwargs['channel_seed'] - static_remotekey = kwargs.pop('static_remotekey') node = BIP32Node.from_rootseed(channel_seed, xtype='standard') keypair_generator = lambda family: generate_keypair(node, family) kwargs['per_commitment_secret_seed'] = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey @@ -195,7 +203,22 @@ def from_seed(self, **kwargs): kwargs['htlc_basepoint'] = keypair_generator(LnKeyFamily.HTLC_BASE) kwargs['delayed_basepoint'] = keypair_generator(LnKeyFamily.DELAY_BASE) kwargs['revocation_basepoint'] = keypair_generator(LnKeyFamily.REVOCATION_BASE) - kwargs['payment_basepoint'] = OnlyPubkeyKeypair(static_remotekey) if static_remotekey else keypair_generator(LnKeyFamily.PAYMENT_BASE) + static_remotekey = kwargs.pop('static_remotekey') + static_payment_key = kwargs.pop('static_payment_key') + if static_payment_key: + # We derive the payment_basepoint from a static secret (derived from + # the wallet seed) and a public nonce that is revealed + # when the funding transaction is spent. This way we can restore the + # payment_basepoint, needed for sweeping in the event of a force close. + kwargs['payment_basepoint'] = derive_payment_basepoint( + static_payment_secret=static_payment_key.privkey, + funding_pubkey=kwargs['multisig_key'].pubkey + ) + elif static_remotekey: # we automatically sweep to a wallet address + kwargs['payment_basepoint'] = OnlyPubkeyKeypair(static_remotekey) + else: # legacy channel with key rotation + kwargs['payment_basepoint'] = keypair_generator(LnKeyFamily.PAYMENT_BASE) + return LocalConfig(**kwargs) def validate_params(self, *, funding_sat: int) -> None: @@ -506,7 +529,25 @@ def derive_blinded_privkey(basepoint_secret: bytes, per_commitment_secret: bytes return int.to_bytes(sum, length=32, byteorder='big', signed=False) -def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_delayedpubkey, success, to_self_delay): +def derive_payment_basepoint(static_payment_secret: bytes, funding_pubkey: bytes) -> Keypair: + assert isinstance(static_payment_secret, bytes) + assert isinstance(funding_pubkey, bytes) + payment_basepoint = ecc.ECPrivkey(sha256(static_payment_secret + funding_pubkey)) + return Keypair( + pubkey=payment_basepoint.get_public_key_bytes(), + privkey=payment_basepoint.get_secret_bytes() + ) + + +def make_htlc_tx_output( + amount_msat, + local_feerate, + revocationpubkey, + local_delayedpubkey, + success, + to_self_delay, + has_anchors: bool +): assert type(amount_msat) is int assert type(local_feerate) is int script = make_commitment_output_to_local_witness_script( @@ -516,7 +557,7 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela ) p2wsh = bitcoin.redeem_script_to_address('p2wsh', bh2u(script)) - weight = HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT + weight = effective_htlc_tx_weight(success=success, has_anchors=has_anchors) fee = local_feerate * weight fee = fee // 1000 * 1000 final_amount_sat = (amount_msat - fee) // 1000 @@ -552,13 +593,18 @@ def make_htlc_tx(*, cltv_expiry: int, inputs: List[PartialTxInput], output: Part tx = PartialTransaction.from_io(inputs, c_outputs, locktime=cltv_expiry, version=2) return tx -def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, - local_htlcpubkey: bytes, payment_hash: bytes) -> bytes: +def make_offered_htlc( + revocation_pubkey: bytes, + remote_htlcpubkey: bytes, + local_htlcpubkey: bytes, + payment_hash: bytes, + has_anchors: bool, +) -> bytes: assert type(revocation_pubkey) is bytes assert type(remote_htlcpubkey) is bytes assert type(local_htlcpubkey) is bytes assert type(payment_hash) is bytes - script = bfh(construct_script([ + script_opcodes = [ opcodes.OP_DUP, opcodes.OP_HASH160, bitcoin.hash_160(revocation_pubkey), @@ -584,17 +630,26 @@ def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, - opcodes.OP_ENDIF, - ])) + ] + if has_anchors: + script_opcodes.extend([1, opcodes.OP_CHECKSEQUENCEVERIFY, opcodes.OP_DROP]) + script_opcodes.append(opcodes.OP_ENDIF) + script = bfh(construct_script(script_opcodes)) return script -def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, - local_htlcpubkey: bytes, payment_hash: bytes, cltv_expiry: int) -> bytes: +def make_received_htlc( + revocation_pubkey: bytes, + remote_htlcpubkey: bytes, + local_htlcpubkey: bytes, + payment_hash: bytes, + cltv_expiry: int, + has_anchors: bool, +) -> bytes: for i in [revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash]: assert type(i) is bytes assert type(cltv_expiry) is int - script = bfh(construct_script([ + script_opcodes = [ opcodes.OP_DUP, opcodes.OP_HASH160, bitcoin.hash_160(revocation_pubkey), @@ -623,23 +678,39 @@ def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, opcodes.OP_DROP, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, - opcodes.OP_ENDIF, - ])) + ] + if has_anchors: + script_opcodes.extend([1, opcodes.OP_CHECKSEQUENCEVERIFY, opcodes.OP_DROP]) + script_opcodes.append(opcodes.OP_ENDIF) + script = bfh(construct_script(script_opcodes)) return script -def make_htlc_output_witness_script(is_received_htlc: bool, remote_revocation_pubkey: bytes, remote_htlc_pubkey: bytes, - local_htlc_pubkey: bytes, payment_hash: bytes, cltv_expiry: Optional[int]) -> bytes: +def make_htlc_output_witness_script( + is_received_htlc: bool, + remote_revocation_pubkey: bytes, + remote_htlc_pubkey: bytes, + local_htlc_pubkey: bytes, + payment_hash: bytes, + cltv_expiry: Optional[int], + has_anchors: bool, +) -> bytes: if is_received_htlc: - return make_received_htlc(revocation_pubkey=remote_revocation_pubkey, - remote_htlcpubkey=remote_htlc_pubkey, - local_htlcpubkey=local_htlc_pubkey, - payment_hash=payment_hash, - cltv_expiry=cltv_expiry) + return make_received_htlc( + revocation_pubkey=remote_revocation_pubkey, + remote_htlcpubkey=remote_htlc_pubkey, + local_htlcpubkey=local_htlc_pubkey, + payment_hash=payment_hash, + cltv_expiry=cltv_expiry, + has_anchors=has_anchors, + ) else: - return make_offered_htlc(revocation_pubkey=remote_revocation_pubkey, - remote_htlcpubkey=remote_htlc_pubkey, - local_htlcpubkey=local_htlc_pubkey, - payment_hash=payment_hash) + return make_offered_htlc( + revocation_pubkey=remote_revocation_pubkey, + remote_htlcpubkey=remote_htlc_pubkey, + local_htlcpubkey=local_htlc_pubkey, + payment_hash=payment_hash, + has_anchors=has_anchors, + ) def get_ordered_channel_configs(chan: 'AbstractChannel', for_us: bool) -> Tuple[Union[LocalConfig, RemoteConfig], @@ -659,12 +730,15 @@ def possible_output_idxs_of_htlc_in_ctx(*, chan: 'Channel', pcp: bytes, subject: other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp) other_htlc_pubkey = derive_pubkey(other_conf.htlc_basepoint.pubkey, pcp) htlc_pubkey = derive_pubkey(conf.htlc_basepoint.pubkey, pcp) - preimage_script = make_htlc_output_witness_script(is_received_htlc=htlc_direction == RECEIVED, - remote_revocation_pubkey=other_revocation_pubkey, - remote_htlc_pubkey=other_htlc_pubkey, - local_htlc_pubkey=htlc_pubkey, - payment_hash=payment_hash, - cltv_expiry=cltv_expiry) + preimage_script = make_htlc_output_witness_script( + is_received_htlc=htlc_direction == RECEIVED, + remote_revocation_pubkey=other_revocation_pubkey, + remote_htlc_pubkey=other_htlc_pubkey, + local_htlc_pubkey=htlc_pubkey, + payment_hash=payment_hash, + cltv_expiry=cltv_expiry, + has_anchors=chan.has_anchors(), + ) htlc_address = redeem_script_to_address('p2wsh', bh2u(preimage_script)) candidates = ctx.get_output_idxs_from_address(htlc_address) return {output_idx for output_idx in candidates @@ -716,22 +790,29 @@ def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTL # if we do not receive, and the commitment tx is not for us, they receive, so it is also an HTLC-success is_htlc_success = htlc_direction == RECEIVED witness_script_of_htlc_tx_output, htlc_tx_output = make_htlc_tx_output( - amount_msat = amount_msat, - local_feerate = chan.get_feerate(subject, ctn=ctn), + amount_msat=amount_msat, + local_feerate=chan.get_feerate(subject, ctn=ctn), revocationpubkey=other_revocation_pubkey, local_delayedpubkey=delayedpubkey, - success = is_htlc_success, - to_self_delay = other_conf.to_self_delay) - preimage_script = make_htlc_output_witness_script(is_received_htlc=is_htlc_success, - remote_revocation_pubkey=other_revocation_pubkey, - remote_htlc_pubkey=other_htlc_pubkey, - local_htlc_pubkey=htlc_pubkey, - payment_hash=payment_hash, - cltv_expiry=cltv_expiry) + success=is_htlc_success, + to_self_delay=other_conf.to_self_delay, + has_anchors=chan.has_anchors(), + ) + preimage_script = make_htlc_output_witness_script( + is_received_htlc=is_htlc_success, + remote_revocation_pubkey=other_revocation_pubkey, + remote_htlc_pubkey=other_htlc_pubkey, + local_htlc_pubkey=htlc_pubkey, + payment_hash=payment_hash, + cltv_expiry=cltv_expiry, + has_anchors=chan.has_anchors(), + ) htlc_tx_inputs = make_htlc_tx_inputs( commit.txid(), ctx_output_idx, amount_msat=amount_msat, witness_script=bh2u(preimage_script)) + if chan.has_anchors(): + htlc_tx_inputs[0].nsequence = 1 if is_htlc_success: cltv_expiry = 0 htlc_tx = make_htlc_tx(cltv_expiry=cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output) @@ -770,41 +851,90 @@ class Direction(IntFlag): LOCAL = HTLCOwner.LOCAL REMOTE = HTLCOwner.REMOTE -def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], local_amount_msat: int, remote_amount_msat: int, - local_script: str, remote_script: str, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]: - # BOLT-03: "Base commitment transaction fees are extracted from the funder's amount; - # if that amount is insufficient, the entire amount of the funder's output is used." - # -> if funder cannot afford feerate, their output might go negative, so take max(0, x) here: - to_local_amt = max(0, local_amount_msat - fees_per_participant[LOCAL]) - to_local = PartialTxOutput(scriptpubkey=bfh(local_script), value=to_local_amt // 1000) - to_remote_amt = max(0, remote_amount_msat - fees_per_participant[REMOTE]) - to_remote = PartialTxOutput(scriptpubkey=bfh(remote_script), value=to_remote_amt // 1000) - non_htlc_outputs = [to_local, to_remote] +def make_commitment_outputs( + *, + fees_per_participant: Mapping[HTLCOwner, int], + local_amount_msat: int, + remote_amount_msat: int, + local_script: str, + remote_script: str, + htlcs: List[ScriptHtlc], + dust_limit_sat: int, + has_anchors: bool, + local_anchor_script: Optional[str], + remote_anchor_script: Optional[str] +) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]: + + # determine HTLC outputs and trim below dust to know if anchors need to be included htlc_outputs = [] for script, htlc in htlcs: addr = bitcoin.redeem_script_to_address('p2wsh', bh2u(script)) - htlc_outputs.append(PartialTxOutput(scriptpubkey=bfh(address_to_script(addr)), - value=htlc.amount_msat // 1000)) + if htlc.amount_msat // 1000 > dust_limit_sat: + htlc_outputs.append( + PartialTxOutput( + scriptpubkey=bfh(address_to_script(addr)), + value=htlc.amount_msat // 1000 + )) + + # BOLT-03: "Base commitment transaction fees are extracted from the funder's amount; + # if that amount is insufficient, the entire amount of the funder's output is used." + non_htlc_outputs = [] + to_local_amt_msat = local_amount_msat - fees_per_participant[LOCAL] + to_remote_amt_msat = remote_amount_msat - fees_per_participant[REMOTE] + + anchor_outputs = [] + # if no anchor scripts are set, we ignore anchor outputs, useful when this + # function is used to determine outputs for a collaborative close + if has_anchors and local_anchor_script and remote_anchor_script: + local_pays_anchors = bool(fees_per_participant[LOCAL]) + # we always allocate for two anchor outputs even if they are not added + if local_pays_anchors: + to_local_amt_msat -= 2 * FIXED_ANCHOR_SAT * 1000 + else: + to_remote_amt_msat -= 2 * FIXED_ANCHOR_SAT * 1000 + + # include anchors for outputs that materialize, include both if there are HTLCs present + if to_local_amt_msat // 1000 >= dust_limit_sat or htlc_outputs: + anchor_outputs.append(PartialTxOutput(scriptpubkey=bfh(local_anchor_script), value=FIXED_ANCHOR_SAT)) + if to_remote_amt_msat // 1000 >= dust_limit_sat or htlc_outputs: + anchor_outputs.append(PartialTxOutput(scriptpubkey=bfh(remote_anchor_script), value=FIXED_ANCHOR_SAT)) + + # if funder cannot afford feerate, their output might go negative, so take max(0, x) here + to_local_amt_msat = max(0, to_local_amt_msat) + to_remote_amt_msat = max(0, to_remote_amt_msat) + non_htlc_outputs.append(PartialTxOutput(scriptpubkey=bfh(local_script), value=to_local_amt_msat // 1000)) + non_htlc_outputs.append(PartialTxOutput(scriptpubkey=bfh(remote_script), value=to_remote_amt_msat // 1000)) - # trim outputs c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs)) - return htlc_outputs, c_outputs_filtered + c_outputs = c_outputs_filtered + anchor_outputs + return htlc_outputs, c_outputs -def offered_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int) -> int: +def effective_htlc_tx_weight(success: bool, has_anchors: bool): + # for anchors-zero-fee-htlc we set an effective weight of zero + # we only trim htlcs below dust, as in the anchors commitment format, + # the fees for the hltc transaction don't need to be subtracted from + # the htlc output, but fees are taken from extra attached inputs + if has_anchors: + return 0 * HTLC_SUCCESS_WEIGHT_ANCHORS if success else 0 * HTLC_TIMEOUT_WEIGHT_ANCHORS + else: + return HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT + + +def offered_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int, has_anchors: bool) -> int: # offered htlcs strictly below this amount will be trimmed (from ctx). # feerate is in sat/kw # returns value in sat - weight = HTLC_TIMEOUT_WEIGHT + weight = effective_htlc_tx_weight(success=False, has_anchors=has_anchors) return dust_limit_sat + weight * feerate // 1000 -def received_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int) -> int: +def received_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int, has_anchors: bool) -> int: # received htlcs strictly below this amount will be trimmed (from ctx). # feerate is in sat/kw # returns value in sat - weight = HTLC_SUCCESS_WEIGHT + weight = effective_htlc_tx_weight(success=True, has_anchors=has_anchors) return dust_limit_sat + weight * feerate // 1000 @@ -815,13 +945,17 @@ def fee_for_htlc_output(*, feerate: int) -> int: def calc_fees_for_commitment_tx(*, num_htlcs: int, feerate: int, - is_local_initiator: bool, round_to_sat: bool = True) -> Dict['HTLCOwner', int]: + is_local_initiator: bool, round_to_sat: bool = True, has_anchors: bool) -> Dict['HTLCOwner', int]: # feerate is in sat/kw # returns fees in msats # note: BOLT-02 specifies that msat fees need to be rounded down to sat. # However, the rounding needs to happen for the total fees, so if the return value # is to be used as part of additional fee calculation then rounding should be done after that. - overall_weight = COMMITMENT_TX_WEIGHT + num_htlcs * HTLC_OUTPUT_WEIGHT + if has_anchors: + commitment_tx_weight = COMMITMENT_TX_WEIGHT_ANCHORS + else: + commitment_tx_weight = COMMITMENT_TX_WEIGHT + overall_weight = commitment_tx_weight + num_htlcs * HTLC_OUTPUT_WEIGHT fee = feerate * overall_weight if round_to_sat: fee = fee // 1000 * 1000 @@ -849,7 +983,8 @@ def make_commitment( remote_amount: int, dust_limit_sat: int, fees_per_participant: Mapping[HTLCOwner, int], - htlcs: List[ScriptHtlc] + htlcs: List[ScriptHtlc], + has_anchors: bool ) -> PartialTransaction: c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey, funding_pos, funding_txid, funding_sat) @@ -862,7 +997,12 @@ def make_commitment( # commitment tx outputs local_address = make_commitment_output_to_local_address(revocation_pubkey, to_self_delay, delayed_pubkey) - remote_address = make_commitment_output_to_remote_address(remote_payment_pubkey) + remote_address = make_commitment_output_to_remote_address(remote_payment_pubkey, has_anchors) + local_anchor_address = None + remote_anchor_address = None + if has_anchors: + local_anchor_address = make_commitment_output_to_anchor_address(local_funding_pubkey) + remote_anchor_address = make_commitment_output_to_anchor_address(remote_funding_pubkey) # note: it is assumed that the given 'htlcs' are all non-dust (dust htlcs already trimmed) # BOLT-03: "Transaction Input and Output Ordering @@ -879,7 +1019,11 @@ def make_commitment( local_script=address_to_script(local_address), remote_script=address_to_script(remote_address), htlcs=htlcs, - dust_limit_sat=dust_limit_sat) + dust_limit_sat=dust_limit_sat, + has_anchors=has_anchors, + local_anchor_script=address_to_script(local_anchor_address) if local_anchor_address else None, + remote_anchor_script=address_to_script(remote_anchor_address) if remote_anchor_address else None + ) assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat) @@ -911,8 +1055,39 @@ def make_commitment_output_to_local_address( local_script = make_commitment_output_to_local_witness_script(revocation_pubkey, to_self_delay, delayed_pubkey) return bitcoin.redeem_script_to_address('p2wsh', bh2u(local_script)) -def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str: - return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey)) +def make_commitment_output_to_remote_witness_script(remote_payment_pubkey: bytes) -> bytes: + assert isinstance(remote_payment_pubkey, bytes) + script = bfh(construct_script([ + remote_payment_pubkey, + opcodes.OP_CHECKSIGVERIFY, + opcodes.OP_1, + opcodes.OP_CHECKSEQUENCEVERIFY, + ])) + return script + +def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes, has_anchors: bool) -> str: + if has_anchors: + remote_script = make_commitment_output_to_remote_witness_script(remote_payment_pubkey) + return bitcoin.redeem_script_to_address('p2wsh', bh2u(remote_script)) + else: + return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey)) + +def make_commitment_output_to_anchor_witness_script(funding_pubkey: bytes) -> bytes: + assert isinstance(funding_pubkey, bytes) + script = bfh(construct_script([ + funding_pubkey, + opcodes.OP_CHECKSIG, + opcodes.OP_IFDUP, + opcodes.OP_NOTIF, + opcodes.OP_16, + opcodes.OP_CHECKSEQUENCEVERIFY, + opcodes.OP_ENDIF, + ])) + return script + +def make_commitment_output_to_anchor_address(funding_pubkey: bytes) -> str: + script = make_commitment_output_to_anchor_witness_script(funding_pubkey) + return bitcoin.redeem_script_to_address('p2wsh', bh2u(script)) def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config): tx.sign({bh2u(local_config.multisig_key.pubkey): (local_config.multisig_key.privkey, True)}) @@ -947,6 +1122,15 @@ 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 + + def get_ecdh(priv: bytes, pub: bytes) -> bytes: pt = ECPubkey(pub) * string_to_number(priv) return sha256(pt.get_public_key_bytes()) @@ -1017,6 +1201,18 @@ class LnFeatures(IntFlag): _ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_OPT] = (LNFC.INIT | LNFC.NODE_ANN) _ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_REQ] = (LNFC.INIT | LNFC.NODE_ANN) + OPTION_ANCHOR_OUTPUTS_REQ = 1 << 20 + OPTION_ANCHOR_OUTPUTS_OPT = 1 << 21 + _ln_feature_direct_dependencies[OPTION_ANCHOR_OUTPUTS_OPT] = {OPTION_STATIC_REMOTEKEY_OPT} + _ln_feature_contexts[OPTION_ANCHOR_OUTPUTS_REQ] = (LNFC.INIT | LNFC.NODE_ANN) + _ln_feature_contexts[OPTION_ANCHOR_OUTPUTS_OPT] = (LNFC.INIT | LNFC.NODE_ANN) + + OPTION_ANCHORS_ZERO_FEE_HTLC_REQ = 1 << 22 + OPTION_ANCHORS_ZERO_FEE_HTLC_OPT = 1 << 23 + _ln_feature_direct_dependencies[OPTION_ANCHORS_ZERO_FEE_HTLC_OPT] = {OPTION_STATIC_REMOTEKEY_OPT} + _ln_feature_contexts[OPTION_ANCHORS_ZERO_FEE_HTLC_REQ] = (LNFC.INIT | LNFC.NODE_ANN) + _ln_feature_contexts[OPTION_ANCHORS_ZERO_FEE_HTLC_OPT] = (LNFC.INIT | LNFC.NODE_ANN) + OPTION_TRAMPOLINE_ROUTING_REQ = 1 << 24 OPTION_TRAMPOLINE_ROUTING_OPT = 1 << 25 @@ -1118,6 +1314,7 @@ def supports(self, feature: 'LnFeatures') -> bool: | LnFeatures.BASIC_MPP_OPT | LnFeatures.BASIC_MPP_REQ | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ + | LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT | LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_REQ ) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index fa2aba339079..58c5e8d3db17 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -186,8 +186,8 @@ async def check_onchain_situation(self, address, funding_outpoint): # early return if address has not been added yet if not self.is_mine(address): return - spenders = self.inspect_tx_candidate(funding_outpoint, 0) - # inspect_tx_candidate might have added new addresses, in which case we return ealy + spenders = self.inspect_tx_candidate(funding_outpoint, 0) # outpoint -> txid + # inspect_tx_candidate might have added new addresses, in which case we return early if not self.is_up_to_date(): return funding_txid = funding_outpoint.split(':')[0] @@ -197,7 +197,7 @@ async def check_onchain_situation(self, address, funding_outpoint): if closing_txid: closing_tx = self.db.get_transaction(closing_txid) if closing_tx: - keep_watching = await self.do_breach_remedy(funding_outpoint, closing_tx, spenders) + keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx, spenders) else: self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...") keep_watching = True @@ -213,7 +213,7 @@ async def check_onchain_situation(self, address, funding_outpoint): if not keep_watching: await self.unwatch_channel(address, funding_outpoint) - async def do_breach_remedy(self, funding_outpoint, closing_tx, spenders) -> bool: + async def sweep_commitment_transaction(self, funding_outpoint, closing_tx, spenders) -> bool: raise NotImplementedError() # implemented by subclasses async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str, @@ -221,7 +221,9 @@ async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str closing_height: TxMinedInfo, keep_watching: bool) -> None: raise NotImplementedError() # implemented by subclasses - def inspect_tx_candidate(self, outpoint, n): + def inspect_tx_candidate(self, outpoint, n: int) -> Dict[str, str]: + """Recursively retrieves spenders of outpoint with maximal depth of 1. + n is the starting level of the spender.""" prev_txid, index = outpoint.split(':') txid = self.db.get_spent_outpoint(prev_txid, int(index)) result = {outpoint:txid} @@ -287,7 +289,7 @@ async def start_watching(self): for outpoint, address in random_shuffled_copy(lst): self.add_channel(outpoint, address) - async def do_breach_remedy(self, funding_outpoint, closing_tx, spenders): + async def sweep_commitment_transaction(self, funding_outpoint, closing_tx, spenders): keep_watching = False for prevout, spender in spenders.items(): if spender is not None: @@ -372,58 +374,86 @@ async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str keep_watching=keep_watching) await self.lnworker.on_channel_update(chan) - async def do_breach_remedy(self, funding_outpoint, closing_tx, spenders): + async def sweep_commitment_transaction(self, funding_outpoint, closing_tx, spenders) -> bool: + """This function is called when a channel was closed. In this case + we need to check for redeemable outputs of the commitment transaction + or spenders down the line (HTLC-timeout/success transactions). + + Returns whether we should continue to monitor.""" chan = self.lnworker.channel_by_txo(funding_outpoint) if not chan: return False chan_id_for_log = chan.get_id_for_log() - # detect who closed and set sweep_info - sweep_info_dict = chan.sweep_ctx(closing_tx) + # detect who closed and get information about how to claim outputs + 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 transaction - for prevout, sweep_info in sweep_info_dict.items(): + + # create and broadcast transactions + 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() - spender_txid = spenders.get(prevout) + # the output is swept by a certain txid that we know of + spender_txid = spenders.get(swept_output) if spender_txid is not None: + # was output already swept and published? spender_tx = self.db.get_transaction(spender_txid) if not spender_tx: keep_watching = True continue - 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) + + # 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 + 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', e_htlc_tx, 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: - keep_watching |= not self.is_deeply_mined(spender_txid) - txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout)) - assert txin_idx is not None - spender_txin = spender_tx.inputs()[txin_idx] - chan.extract_preimage_from_htlc_txin(spender_txin) - else: - await self.try_redeem(prevout, sweep_info, chan_id_for_log, name) + 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 return keep_watching @log_exceptions async def try_redeem(self, prevout: str, sweep_info: 'SweepInfo', chan_id_for_log: str, name: str) -> None: + # prevout is needed to check if previous transaction has enough confirmations prev_txid, prev_index = prevout.split(':') broadcast = True local_height = self.network.get_local_height() - if sweep_info.cltv_expiry: - wanted_height = sweep_info.cltv_expiry - local_height - if wanted_height - local_height > 0: + if sweep_info.cltv_expiry: # HTLC-timeout transaction + # expiry needs to be in the past + wanted_height = sweep_info.cltv_expiry + 1 + if not(local_height > sweep_info.cltv_expiry): broadcast = False reason = 'waiting for {}: CLTV ({} > {}), prevout {}'.format(name, local_height, sweep_info.cltv_expiry, prevout) - if sweep_info.csv_delay: + if sweep_info.csv_delay: # to local, anchors additional: to remote, anchor outputs, HTLC-success, HTLC-timeout + # number of confirmations need to be equal or greater than csv prev_height = self.get_tx_height(prev_txid) wanted_height = sweep_info.csv_delay + prev_height.height - 1 - if wanted_height - local_height > 0: + if not(prev_height.conf >= sweep_info.csv_delay): # number of confirmations need to be equal or greater than csv TODO: please crosscheck broadcast = False reason = 'waiting for {}: CSV ({} >= {}), prevout: {}'.format(name, prev_height.conf, sweep_info.csv_delay, prevout) + if not (sweep_info.cltv_expiry or sweep_info.csv_delay): + # used to control settling of htlcs onchain for testing purposes + # careful, this prevents revocation as well + if not self.lnworker.enable_htlc_settle_onchain: + return tx = sweep_info.gen_tx() if tx is None: self.logger.info(f'{name} could not claim output: {prevout}, dust') diff --git a/electrum/lnworker.py b/electrum/lnworker.py index b8ababb27145..d83722971d0e 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -61,7 +61,7 @@ NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner, UpdateAddHtlc, Direction, LnFeatures, ShortChannelID, HtlcLog, derive_payment_secret_from_payment_preimage, - NoPathFound, InvalidGossipMsg) + NoPathFound, InvalidGossipMsg, UserFacingException) from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .lnonion import OnionFailureCode, OnionRoutingFailure @@ -199,6 +199,7 @@ def __init__(self, xprv, features: LnFeatures): self.lock = threading.RLock() self.node_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NODE_KEY) self.backup_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.BACKUP_CIPHER).privkey + self.static_payment_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.PAYMENT_BASE) self._peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer # needs self.lock self.taskgroup = SilentTaskGroup() self.listen_server = None # type: Optional[asyncio.AbstractServer] @@ -620,6 +621,7 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.logs = defaultdict(list) # type: Dict[str, List[HtlcLog]] # key is RHASH # (not persisted) # used in tests self.enable_htlc_settle = True + self.enable_htlc_settle_onchain = True self.enable_htlc_forwarding = True # note: accessing channels (besides simple lookup) needs self.lock! @@ -649,6 +651,19 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.trampoline_forwarding_failures = {} # todo: should be persisted # map forwarded htlcs (fw_info=(scid_hex, htlc_id)) to originating peer pubkeys self.downstream_htlc_to_upstream_peer_map = {} # type: Dict[Tuple[str, int], bytes] + self.wallet_password = None # used for automatic signing in case of anchor channels + + def maybe_enable_anchors_store_password(self, password): + # for anchor commitments we need the password to be able to spend wallet UTXOs + if self.config.get('enable_anchor_channels'): + if not self.wallet.can_sign_without_user_interaction_if_have_password(): + raise UserFacingException("Wallets that don't support automatic signing cannot use anchor channels.") + self.logger.info("anchor channels are enabled") + self.features |= LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT + self.wallet_password = password + else: + if self.has_anchor_channels(): + raise UserFacingException("Config option 'enable_anchor_channels' must be set with existing anchor channels.") def has_deterministic_node_id(self) -> bool: return bool(self.db.get('lightning_xprv')) @@ -670,6 +685,9 @@ def channels(self) -> Mapping[bytes, Channel]: with self.lock: return self._channels.copy() + def has_anchor_channels(self) -> bool: + return any(channel.has_anchors() for channel in self.channels.values()) + @property def channel_backups(self) -> Mapping[bytes, ChannelBackup]: """Returns a read-only copy of channels.""" diff --git a/electrum/tests/__init__.py b/electrum/tests/__init__.py index dbfc9ada07c1..9c5424175a0b 100644 --- a/electrum/tests/__init__.py +++ b/electrum/tests/__init__.py @@ -34,6 +34,7 @@ def tearDown(self): class ElectrumTestCase(SequentialTestCase): """Base class for our unit tests.""" + TEST_ANCHOR_CHANNELS = False def setUp(self): super().setUp() diff --git a/electrum/tests/anchor-vectors.json b/electrum/tests/anchor-vectors.json new file mode 100644 index 000000000000..ac438fc0867c --- /dev/null +++ b/electrum/tests/anchor-vectors.json @@ -0,0 +1,241 @@ +[ + { + "Name": "simple commitment tx with no HTLCs", + "LocalBalance": 7000000000, + "RemoteBalance": 3000000000, + "FeePerKw": 15000, + "UseTestHtlcs": false, + "HtlcDescs": [], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80044a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994c0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a508b6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004830450221008266ac6db5ea71aac3c95d97b0e172ff596844851a3216eb88382a8dddfd33d2022050e240974cfd5d708708b4365574517c18e7ae535ef732a3484d43d0d82be9f701483045022100f89034eba16b2be0e5581f750a0a6309192b75cce0f202f0ee2b4ec0cc394850022076c65dc507fe42276152b7a3d90e961e678adbe966e916ecfe85e64d430e75f301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "3045022100f89034eba16b2be0e5581f750a0a6309192b75cce0f202f0ee2b4ec0cc394850022076c65dc507fe42276152b7a3d90e961e678adbe966e916ecfe85e64d430e75f3" + }, + { + "Name": "simple commitment tx with no HTLCs and single anchor", + "LocalBalance": 7000000000, + "RemoteBalance": 0, + "FeePerKw": 15000, + "UseTestHtlcs": false, + "HtlcDescs": [], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80024a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f508b6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100da5310620e72bc23dc57af25d18102cc75479aea0258ab89fe1a66ca176033ec0220339efb450c12872e134c8bda986bb92f3e4eebcaa2d0fee5d9a2b1257d12f12a0147304402200dc30542c9b8b2ff4b8d98f46798b3218a088a07e97b9e786177287dc6a5347b02203d23b1c2bf17262362fdb4cdcc36dbb449a9efcdb10051ad52cfa09fc76842b001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "304402200dc30542c9b8b2ff4b8d98f46798b3218a088a07e97b9e786177287dc6a5347b02203d23b1c2bf17262362fdb4cdcc36dbb449a9efcdb10051ad52cfa09fc76842b0" + }, + { + "Name": "commitment tx with seven outputs untrimmed (maximum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 644, + "UseTestHtlcs": true, + "HtlcDescs": [ + { + "RemoteSigHex": "304402205912d91c58016f593d9e46fefcdb6f4125055c41a17b03101eaaa034b9028ab60220520d4d239c85c66e4c75c5b413620b62736e227659d7821b308e2b8ced3e728e", + "ResolutionTxHex": "02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a0200000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402205912d91c58016f593d9e46fefcdb6f4125055c41a17b03101eaaa034b9028ab60220520d4d239c85c66e4c75c5b413620b62736e227659d7821b308e2b8ced3e728e834730440220473166a5adcca68550bab80403f410a726b5bd855030527e3fefa8c1e4b4fd7b02203b1dc91d8d69039473036cb5c34398b99e8eb90ae500c22130a557b62294b188012000000000000000000000000000000000000000000000000000000000000000008d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6851b2756800000000" + }, + { + "RemoteSigHex": "3045022100c6b4113678039ee1e43a6cba5e3224ed2355ffc05e365a393afe8843dc9a76860220566d01fd52d65a89ba8595023884f9e8f2e9a310a6b9b85281c0bce06863430c", + "ResolutionTxHex": "02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a0300000000010000000124060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c6b4113678039ee1e43a6cba5e3224ed2355ffc05e365a393afe8843dc9a76860220566d01fd52d65a89ba8595023884f9e8f2e9a310a6b9b85281c0bce06863430c83483045022100d0d86307ea55d5daa80f453ad6d64b78fe8a6504aac25407c73e8502c0702c1602206a0809a02aa00c8dc4a53d976bb05d4605d8bb0b7b26b973a5c4e2734d8afbb401008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000" + }, + { + "RemoteSigHex": "304402203c3a699fb80a38112aafd73d6e3a9b7d40bc2c3ed8b7fbc182a20f43b215172202204e71821b984d1af52c4b8e2cd4c572578c12a965866130c2345f61e4c2d3fef4", + "ResolutionTxHex": "02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a040000000001000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402203c3a699fb80a38112aafd73d6e3a9b7d40bc2c3ed8b7fbc182a20f43b215172202204e71821b984d1af52c4b8e2cd4c572578c12a965866130c2345f61e4c2d3fef48347304402205bcfa92f83c69289a412b0b6dd4f2a0fe0b0fc2d45bd74706e963257a09ea24902203783e47883e60b86240e877fcbf33d50b1742f65bc93b3162d1be26583b367ee012001010101010101010101010101010101010101010101010101010101010101018d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6851b2756800000000" + }, + { + "RemoteSigHex": "304402200f089bcd20f25475216307d32aa5b6c857419624bfba1da07335f51f6ba4645b02206ce0f7153edfba23b0d4b2afc26bb3157d404368cb8ea0ca7cf78590dcdd28cf", + "ResolutionTxHex": "02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a050000000001000000010c0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402200f089bcd20f25475216307d32aa5b6c857419624bfba1da07335f51f6ba4645b02206ce0f7153edfba23b0d4b2afc26bb3157d404368cb8ea0ca7cf78590dcdd28cf83483045022100e4516da08f72c7a4f7b2f37aa84a0feb54ae2cc5b73f0da378e81ae0ca8119bf02207751b2628d8e2f62b4b9abccda4866246c1bfcc82e3d416ad562fd212102c28f01008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000" + }, + { + "RemoteSigHex": "3045022100aa72cfaf0965020c73a12c77276c6411ca68c4de36ac1998adf86c917a899a43022060da0a159fecfe0bed37c3962d767f12f90e30fed8a8f34b1301775c21a2bd3a", + "ResolutionTxHex": "02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a06000000000100000001da0d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100aa72cfaf0965020c73a12c77276c6411ca68c4de36ac1998adf86c917a899a43022060da0a159fecfe0bed37c3962d767f12f90e30fed8a8f34b1301775c21a2bd3a8347304402203cd12065c2a42963c762e6b1a981e17695616ecb6f9fb33d8b0717cdd7ca0ee4022065500005c491c1dcf2fe9c4024f74b1c90785d572527055a491278f901143904012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000" + } + ], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80094a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994e80300000000000022002010f88bf09e56f14fb4543fd26e47b0db50ea5de9cf3fc46434792471082621aed0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837ead007000000000000220020fe0598d74fee2205cc3672e6e6647706b4f3099713b4661b62482c3addd04a5eb80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a4f996a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100ef82a405364bfc4007e63a7cc82925a513d79065bdbc216d60b6a4223a323f8a02200716730b8561f3c6d362eaf47f202e99fb30d0557b61b92b5f9134f8e2de368101483045022100e0106830467a558c07544a3de7715610c1147062e7d091deeebe8b5c661cda9402202ad049c1a6d04834317a78483f723c205c9f638d17222aafc620800cc1b6ae3501475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "3045022100e0106830467a558c07544a3de7715610c1147062e7d091deeebe8b5c661cda9402202ad049c1a6d04834317a78483f723c205c9f638d17222aafc620800cc1b6ae35" + }, + { + "Name": "commitment tx with six outputs untrimmed (minimum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 645, + "UseTestHtlcs": true, + "HtlcDescs": [ + { + "RemoteSigHex": "30440220446f9e5c375db6a61d6eeee8b59219a30a4a37372afc2670a1a2889c78e9b943022061895f6088fb48b490ab2140a4842c277b64bf25ff591625dd0356e0c96ab7a8", + "ResolutionTxHex": "02000000000101104f394af4c4fad78337f95e3e9f802f4c0d86ab231853af09b28534856132000200000000010000000123060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220446f9e5c375db6a61d6eeee8b59219a30a4a37372afc2670a1a2889c78e9b943022061895f6088fb48b490ab2140a4842c277b64bf25ff591625dd0356e0c96ab7a883483045022100c1621ba26a99c263fd885feff5fda5ca2cc73df080b3a49ecf15164ee244d2a5022037f4cc7fd4441af39a83a0e44c3b1db7d64a4c8080e8697f9e952f85421a34d801008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000" + }, + { + "RemoteSigHex": "3044022027a3ffcb8a007e3349d75382efbd4b3fb99fcbd479a18555e58697bd1278d5c402205c8303d46211c3ae8975fe84a0df08b4623119fecd03bc93b49d7f7a0c64c710", + "ResolutionTxHex": "02000000000101104f394af4c4fad78337f95e3e9f802f4c0d86ab231853af09b28534856132000300000000010000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022027a3ffcb8a007e3349d75382efbd4b3fb99fcbd479a18555e58697bd1278d5c402205c8303d46211c3ae8975fe84a0df08b4623119fecd03bc93b49d7f7a0c64c71083483045022100b697aca55c6fb15e5348bb7387b584815fd15e8dd306afe0c477cb550d0c2d40022050b0f7e370f7604d2fec781fefe86715dbe95dff4dab88d628f509d62f854de1012001010101010101010101010101010101010101010101010101010101010101018d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6851b2756800000000" + }, + { + "RemoteSigHex": "30440220013975ae356e6daf22a86a29f21c4f35aca82ed8f731a1103c60c74f5ed1c5aa02200350d4e5455cdbcacb7ccf174db5bed8286019e509a113f6b4c5e606ee12c9d7", + "ResolutionTxHex": "02000000000101104f394af4c4fad78337f95e3e9f802f4c0d86ab231853af09b2853485613200040000000001000000010b0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220013975ae356e6daf22a86a29f21c4f35aca82ed8f731a1103c60c74f5ed1c5aa02200350d4e5455cdbcacb7ccf174db5bed8286019e509a113f6b4c5e606ee12c9d783483045022100e69a29f78779577830e73f327073c93168896f1b89432124b7846f5def9cd9cb02204433db3697e6ed7ac89574ca066a749640e0c9e114ac2e0ee4545741fcf7b7e901008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000" + }, + { + "RemoteSigHex": "304402205257017423644c7e831f30bc0c334eecfe66e9a6d2e92d157c5bece576b2be4f022047b21cf8e955e22b7471940563922d1a5852fb95459ca32905c7d46a19141664", + "ResolutionTxHex": "02000000000101104f394af4c4fad78337f95e3e9f802f4c0d86ab231853af09b285348561320005000000000100000001d90d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402205257017423644c7e831f30bc0c334eecfe66e9a6d2e92d157c5bece576b2be4f022047b21cf8e955e22b7471940563922d1a5852fb95459ca32905c7d46a191416648347304402204f5de65a624e3f757adffb678bd887eb4e656538c5ea7044922f6ee3eed8a06202206ff6f7bfe73b565343cae76131ac658f1a9c60d3ca2343358cda60b9e35f94c8012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000" + } + ], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80084a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994d0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837ead007000000000000220020fe0598d74fee2205cc3672e6e6647706b4f3099713b4661b62482c3addd04a5eb80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994abc996a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100d57697c707b6f6d053febf24b98e8989f186eea42e37e9e91663ec2c70bb8f70022079b0715a472118f262f43016a674f59c015d9cafccec885968e76d9d9c5d005101473044022025d97466c8049e955a5afce28e322f4b34d2561118e52332fb400f9b908cc0a402205dc6fba3a0d67ee142c428c535580cd1f2ff42e2f89b47e0c8a01847caffc31201475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "3044022025d97466c8049e955a5afce28e322f4b34d2561118e52332fb400f9b908cc0a402205dc6fba3a0d67ee142c428c535580cd1f2ff42e2f89b47e0c8a01847caffc312" + }, + { + "Name": "commitment tx with six outputs untrimmed (maximum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 2060, + "UseTestHtlcs": true, + "HtlcDescs": [ + { + "RemoteSigHex": "30440220011f999016570bbab9f3125377d0f35096b4dbe155f97c20f71829ead2817d1602201f23f7e17f6928734601c5d8613431eed5c90aa41c3106e8c1cb02ce32aacb5d", + "ResolutionTxHex": "02000000000101e7f364cf3a554b670767e723ef14b2af7a3eac70bd79dbde9256f384369c062d0200000000010000000175020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220011f999016570bbab9f3125377d0f35096b4dbe155f97c20f71829ead2817d1602201f23f7e17f6928734601c5d8613431eed5c90aa41c3106e8c1cb02ce32aacb5d83473044022017da96dfb0eb4061fa0162dc6fa6b2e07ecc5040ab5e6cb07be59838460b3e58022079371ffc95002cc1dc2891ec38198c9c25aca8164304fe114f1b55e2ffd1ddd501008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000" + }, + { + "RemoteSigHex": "304402202d2d9681409b0a0987bd4a268ffeb112df85c4c988ac2a3a2475cb00a61912c302206aa4f4d1388b7d3282bc847871af3cca30766cc8f1064e3a41ec7e82221e10f7", + "ResolutionTxHex": "02000000000101e7f364cf3a554b670767e723ef14b2af7a3eac70bd79dbde9256f384369c062d0300000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202d2d9681409b0a0987bd4a268ffeb112df85c4c988ac2a3a2475cb00a61912c302206aa4f4d1388b7d3282bc847871af3cca30766cc8f1064e3a41ec7e82221e10f78347304402206426d67911aa6ff9b1cb147b093f3f65a37831a86d7c741d999afc0666e1773d022000bb71821650c70ea58d9bcdd03af736c41a5a8159d436c3ee0408a07394dcce012001010101010101010101010101010101010101010101010101010101010101018d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6851b2756800000000" + }, + { + "RemoteSigHex": "3045022100f51cdaa525b7d4304548c642bb7945215eb5ae7d32874517cde67ca23ab0a12202206286d59e4b19926c6ac844be6f3ab8149a1ddb9c70f5026b7e83e40a6c08e6e1", + "ResolutionTxHex": "02000000000101e7f364cf3a554b670767e723ef14b2af7a3eac70bd79dbde9256f384369c062d040000000001000000015d060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100f51cdaa525b7d4304548c642bb7945215eb5ae7d32874517cde67ca23ab0a12202206286d59e4b19926c6ac844be6f3ab8149a1ddb9c70f5026b7e83e40a6c08e6e18348304502210091b16b1ac63b867e7a5ca0344f7b2aa1cdd49d4b72eac86a31e7ec6f069e20640220402bfb571ba3a9c49e3b0061c89303453803d0241059d899222aaac4799b507601008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000" + }, + { + "RemoteSigHex": "304402202f058d99cb5a54f90773d43ba4e7a0089efd9f8269ef2da1b85d48a3e230555402205acc4bd6561830867d45cd7b84bba9fa35ad2b345016471c1737142bc99782c4", + "ResolutionTxHex": "02000000000101e7f364cf3a554b670767e723ef14b2af7a3eac70bd79dbde9256f384369c062d05000000000100000001f2090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202f058d99cb5a54f90773d43ba4e7a0089efd9f8269ef2da1b85d48a3e230555402205acc4bd6561830867d45cd7b84bba9fa35ad2b345016471c1737142bc99782c48347304402202913f9cacea54efd2316cffa91219def9e0e111977216c1e76e9da80befab14f022000a9a69e8f37ebe4a39107ab50fab0dde537334588f8f412bbaca57b179b87a6012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000" + } + ], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80084a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994d0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837ead007000000000000220020fe0598d74fee2205cc3672e6e6647706b4f3099713b4661b62482c3addd04a5eb80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994ab88f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402201ce37a44b95213358c20f44404d6db7a6083bea6f58de6c46547ae41a47c9f8202206db1d45be41373e92f90d346381febbea8c78671b28c153e30ad1db3441a94970147304402206208aeb34e404bd052ce3f298dfa832891c9d42caec99fe2a0d2832e9690b94302201b034bfcc6fa9faec667a9b7cbfe0b8d85e954aa239b66277887b5088aff08c301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "304402206208aeb34e404bd052ce3f298dfa832891c9d42caec99fe2a0d2832e9690b94302201b034bfcc6fa9faec667a9b7cbfe0b8d85e954aa239b66277887b5088aff08c3" + }, + { + "Name": "commitment tx with five outputs untrimmed (minimum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 2061, + "UseTestHtlcs": true, + "HtlcDescs": [ + { + "RemoteSigHex": "3045022100e10744f572a2cd1d787c969e894b792afaed21217ee0480df0112d2fa3ef96ea02202af4f66eb6beebc36d8e98719ed6b4be1b181659fcb561fc491d8cfebff3aa85", + "ResolutionTxHex": "02000000000101cf32732fe2d1387ed4e2335f69ddd3c0f337dabc03269e742531f89d35e161d10200000000010000000174020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e10744f572a2cd1d787c969e894b792afaed21217ee0480df0112d2fa3ef96ea02202af4f66eb6beebc36d8e98719ed6b4be1b181659fcb561fc491d8cfebff3aa8583483045022100c3dc3ea50a0ca20e350f97b50c52c5514717cfa36cb9600918caac5cb556842b022049af018d676dde0c8e28ecf325f3ff5c1594261c4f7511d501f9d62d0594d2a201008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000" + }, + { + "RemoteSigHex": "3045022100e1f51fb72fec604b029b348a3bb6363454e1869f5b1e24fd736f860c8039f8070220030a2c90186437d8c9b47d4897798c024521b1274991c4cdc125970b346094b1", + "ResolutionTxHex": "02000000000101cf32732fe2d1387ed4e2335f69ddd3c0f337dabc03269e742531f89d35e161d1030000000001000000015c060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e1f51fb72fec604b029b348a3bb6363454e1869f5b1e24fd736f860c8039f8070220030a2c90186437d8c9b47d4897798c024521b1274991c4cdc125970b346094b183483045022100ec7ade6037e531629f24390ca9713782a04d648065d17fbe6b015981cdb296c202202d61049a6ecba2fb5314f3edcda2361cad187a89bea6e5d15185354d80c0c08501008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000" + }, + { + "RemoteSigHex": "304402203479f81a1d83c516957679dc98bf91d35deada967739a8e3869e3e8db08246130220053c8e154b97e3019048dcec3d51bfaf396f36861fbda6d33f0e2a57155c8b9f", + "ResolutionTxHex": "02000000000101cf32732fe2d1387ed4e2335f69ddd3c0f337dabc03269e742531f89d35e161d104000000000100000001f1090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402203479f81a1d83c516957679dc98bf91d35deada967739a8e3869e3e8db08246130220053c8e154b97e3019048dcec3d51bfaf396f36861fbda6d33f0e2a57155c8b9f83483045022100a558eb5caa04e35a4417c1f0123ac12eec5f6badee28f5764dc6b69486e594f802201589b12784e242f205832d2d032149bd4e79433ec304c05394241fc7dcba5a71012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000" + } + ], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80074a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994d0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837eab80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a18916a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402204ab07c659412dd2cd6043b1ad811ab215e901b6b5653e08cb3d2fe63d3e3dc57022031c7b3d130f9380ef09581f4f5a15cb6f359a2e0a597146b96c3533a26d6f4cd01483045022100a2faf2ad7e323b2a82e07dc40b6847207ca6ad7b089f2c21dea9a4d37e52d59d02204c9480ce0358eb51d92a4342355a97e272e3cc45f86c612a76a3fe32fc3c4cb401475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "3045022100a2faf2ad7e323b2a82e07dc40b6847207ca6ad7b089f2c21dea9a4d37e52d59d02204c9480ce0358eb51d92a4342355a97e272e3cc45f86c612a76a3fe32fc3c4cb4" + }, + { + "Name": "commitment tx with five outputs untrimmed (maximum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 2184, + "UseTestHtlcs": true, + "HtlcDescs": [ + { + "RemoteSigHex": "304402202e03ba1390998b3487e9a7fefcb66814c09abea0ef1bcc915dbaefbcf310569a02206bd10493a105ac69048e9bcedcb8e3301ef81b55018d911a4afd297297f98d30", + "ResolutionTxHex": "020000000001015b03043e20eb467029305a22af4c3b915e793743f192c5d225cf1d3c6e8c03010200000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202e03ba1390998b3487e9a7fefcb66814c09abea0ef1bcc915dbaefbcf310569a02206bd10493a105ac69048e9bcedcb8e3301ef81b55018d911a4afd297297f98d308347304402200c3952ca04be0c60dcc0b7873a0829f560607524943554ae4a27d8d967166199022021a68657b88e22f9bf9ac6065be412685aff643d17049f04f2e99e86197dabb101008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6851b27568f6010000" + }, + { + "RemoteSigHex": "304402201f8a6adda2403bc400c919ea69d72d315337291e00d02cde085ea32953dbc50002202d65230da98df7af8ebefd2b60b457d0945232988ee2d7460a94a77d414a9acc", + "ResolutionTxHex": "020000000001015b03043e20eb467029305a22af4c3b915e793743f192c5d225cf1d3c6e8c0301030000000001000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402201f8a6adda2403bc400c919ea69d72d315337291e00d02cde085ea32953dbc50002202d65230da98df7af8ebefd2b60b457d0945232988ee2d7460a94a77d414a9acc83483045022100ea69c9273b8914ac62b5b7082d6ac1da2b7b065ebf2ef3cd6403f5305ce3f26802203d98736ea97638895a898dfcc5ee0d0c55eb496b3964df0bb25d223688ea8b8701008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000" + }, + { + "RemoteSigHex": "3045022100ea6e4c9b8f56dd9cf5799492a201cdd65b8bc9bc089c3cff34107896ae313f90022034760f7760975cc68e8917a7f62894e25583da7be11af557c4fc402661d0cbf8", + "ResolutionTxHex": "020000000001015b03043e20eb467029305a22af4c3b915e793743f192c5d225cf1d3c6e8c0301040000000001000000019b090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ea6e4c9b8f56dd9cf5799492a201cdd65b8bc9bc089c3cff34107896ae313f90022034760f7760975cc68e8917a7f62894e25583da7be11af557c4fc402661d0cbf8834730440220717012f2f7ef6cac590aaf66c2109132c93ffba245959ac62d82e394ba80191302203f00fd9cb37c92c6b0ad4b33e62c3e55b04e5c2cfa0adcca5a9bc49774eeca8a012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000" + } + ], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80074a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994d0070000000000002200203e68115ae0b15b8de75b6c6bc9af5ac9f01391544e0870dae443a1e8fe7837eab80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a4f906a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220555c05261f72c5b4702d5c83a608630822b473048724b08640d6e75e345094250220448950b74a96a56963928ba5db8b457661a742c855e69d239b3b6ab73de307a301473044022013d326f80ff7607cf366c823fcbbcb7a2b10322484825f151e6c4c756af24b8f02201ba05b9d8beb7cea2947f9f4d9e03f90435e93db2dd48b32eb9ca3f3dd042c7901475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "3044022013d326f80ff7607cf366c823fcbbcb7a2b10322484825f151e6c4c756af24b8f02201ba05b9d8beb7cea2947f9f4d9e03f90435e93db2dd48b32eb9ca3f3dd042c79" + }, + { + "Name": "commitment tx with four outputs untrimmed (minimum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 2185, + "UseTestHtlcs": true, + "HtlcDescs": [ + { + "RemoteSigHex": "304502210094480e38afb41d10fae299224872f19c53abe23c7033a1c0642c48713e7863a10220726dd9456407682667dc4bd9c66975acb3744961770b5002f7eb9c0df9ef2f3e", + "ResolutionTxHex": "02000000000101ac13a7715f80b8e52dda43c6929cade5521bdced3a405da02b443f1ffb1e33cc0200000000010000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050048304502210094480e38afb41d10fae299224872f19c53abe23c7033a1c0642c48713e7863a10220726dd9456407682667dc4bd9c66975acb3744961770b5002f7eb9c0df9ef2f3e8347304402203148dac61513dc0361738cba30cb341a1e580f8acd5ab0149bf65bd670688cf002207e5d9a0fcbbea2c263bc714fa9e9c44d7f582ea447f366119fc614a23de32f1f01008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000" + }, + { + "RemoteSigHex": "304402200dbde868dbc20c6a2433fe8979ba5e3f966b1c2d1aeb615f1c42e9c938b3495402202eec5f663c8b601c2061c1453d35de22597c137d1907a2feaf714d551035cb6e", + "ResolutionTxHex": "02000000000101ac13a7715f80b8e52dda43c6929cade5521bdced3a405da02b443f1ffb1e33cc030000000001000000019a090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402200dbde868dbc20c6a2433fe8979ba5e3f966b1c2d1aeb615f1c42e9c938b3495402202eec5f663c8b601c2061c1453d35de22597c137d1907a2feaf714d551035cb6e83483045022100b896bded41d7feac7af25c19e35c53037c53b50e73cfd01eb4ba139c7fdf231602203a3be049d3d89396c4dc766d82ce31e237da8bc3a93e2c7d35992d1932d9cfeb012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000" + } + ], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80064a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994b80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994ac5916a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100cd8479cfe1edb1e5a1d487391e0451a469c7171e51e680183f19eb4321f20e9b02204eab7d5a6384b1b08e03baa6e4d9748dfd2b5ab2bae7e39604a0d0055bbffdd501473044022040f63a16148cf35c8d3d41827f5ae7f7c3746885bb64d4d1b895892a83812b3e02202fcf95c2bf02c466163b3fa3ced6a24926fbb4035095a96842ef516e86ba54c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "3044022040f63a16148cf35c8d3d41827f5ae7f7c3746885bb64d4d1b895892a83812b3e02202fcf95c2bf02c466163b3fa3ced6a24926fbb4035095a96842ef516e86ba54c0" + }, + { + "Name": "commitment tx with four outputs untrimmed (maximum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 3686, + "UseTestHtlcs": true, + "HtlcDescs": [ + { + "RemoteSigHex": "304402202cfe6618926ca9f1574f8c4659b425e9790b4677ba2248d77901290806130ffe02204ab37bb0287abcdb8b750b018d41a09effe37cb65ff801fa70d3f1a416599841", + "ResolutionTxHex": "020000000001012c32e55722e4b96324d8e5b398d583a20780b25202816adc32dc3157dee731c90200000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202cfe6618926ca9f1574f8c4659b425e9790b4677ba2248d77901290806130ffe02204ab37bb0287abcdb8b750b018d41a09effe37cb65ff801fa70d3f1a41659984183473044022030b318139715e3b34f19be852cc01c1c0e1599e8b926a73df2bfb70dd186ddee022062a2b7398aed9f563b4014da04a1a99debd0ff663ceece68a547df5982dc2d7201008876a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6851b27568f7010000" + }, + { + "RemoteSigHex": "30440220687af8544d335376620a6f4b5412bfd0da48de047c1785674f26e669d4a3ff82022058591c1e3a6c50017427d38a8f756eb685bdab88ec73838eed3530048861f9d5", + "ResolutionTxHex": "020000000001012c32e55722e4b96324d8e5b398d583a20780b25202816adc32dc3157dee731c90300000000010000000176050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220687af8544d335376620a6f4b5412bfd0da48de047c1785674f26e669d4a3ff82022058591c1e3a6c50017427d38a8f756eb685bdab88ec73838eed3530048861f9d5834730440220109f1a62b5a13d28d5b7634dd7693b1d5994eb404c4bb4a9a80aa540d3984d170220307251107ff8499a23e99abce7dda4f1c707c98abddb9405a83de0081cde8ace012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000" + } + ], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80064a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994b80b000000000000220020f96d0334feb64a4f40eb272031d07afcb038db56aa57446d60308c9f8ccadef9a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a29896a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100c268496aad5c3f97f25cf41c1ba5483a12982de29b222051b6de3daa2229413b02207f3c82d77a2c14f0096ed9bb4c34649483bb20fa71f819f71af44de6593e8bb2014730440220784485cf7a0ad7979daf2c858ffdaf5298d0020cea7aea466843e7948223bd9902206031b81d25e02a178c64e62f843577fdcdfc7a1decbbfb54cd895de692df85ca01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "30440220784485cf7a0ad7979daf2c858ffdaf5298d0020cea7aea466843e7948223bd9902206031b81d25e02a178c64e62f843577fdcdfc7a1decbbfb54cd895de692df85ca" + }, + { + "Name": "commitment tx with three outputs untrimmed (minimum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 3687, + "UseTestHtlcs": true, + "HtlcDescs": [ + { + "RemoteSigHex": "3045022100b287bb8e079a62dcb3aaa8b6c67c0f434a87ebf64ab0bcfb2fc14b55576b859f02206d37c2eb5fd04cfc9eb0534c76a28a98da251b84a931377cce307af39dfaed74", + "ResolutionTxHex": "02000000000101542562b326c08e3a076d9cfca2be175041366591da334d8d513ff1686fd95a600200000000010000000175050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100b287bb8e079a62dcb3aaa8b6c67c0f434a87ebf64ab0bcfb2fc14b55576b859f02206d37c2eb5fd04cfc9eb0534c76a28a98da251b84a931377cce307af39dfaed7483483045022100a497c64faea286ec4221f48628086dc6403fd7b60a23c4176e8ebbca15ae70dc0220754e20e968e96cf6421fd2a672c8c26d3bc6e19218cfc8fc2aa51fce026c14b1012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000" + } + ], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80054a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994aa28b6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100c970799bcb33f43179eb43b3378a0a61991cf2923f69b36ef12548c3df0e6d500220413dc27d2e39ee583093adfcb7799be680141738babb31cc7b0669a777a31f5d01483045022100ad6c71569856b2d7ff42e838b4abe74a713426b37f22fa667a195a4c88908c6902202b37272b02a42dc6d9f4f82cab3eaf84ac882d9ed762859e1e75455c2c22837701475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "3045022100ad6c71569856b2d7ff42e838b4abe74a713426b37f22fa667a195a4c88908c6902202b37272b02a42dc6d9f4f82cab3eaf84ac882d9ed762859e1e75455c2c228377" + }, + { + "Name": "commitment tx with three outputs untrimmed (maximum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 4893, + "UseTestHtlcs": true, + "HtlcDescs": [ + { + "RemoteSigHex": "30450221008db80f8531104820b3e894492b4463f074f965b542e1b5c153ddfb108a5ea642022030b203d857a2b3581c2087a7bf17c95d04fadc1c6cdae88c620477f2dccb1ee4", + "ResolutionTxHex": "02000000000101d515a15e9175fd315bb8d4e768f28684801a9e5a9acdfeba34f7b3b3b3a9ba1d0200000000010000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221008db80f8531104820b3e894492b4463f074f965b542e1b5c153ddfb108a5ea642022030b203d857a2b3581c2087a7bf17c95d04fadc1c6cdae88c620477f2dccb1ee483483045022100e5fbae857c47dbfc050a05924bd449fc9804798bd6442002c578437dc34450810220296589bc387645512345299e307116aaac4ce9fc752abcd1936b802d03526312012004040404040404040404040404040404040404040404040404040404040404048d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6851b2756800000000" + } + ], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80054a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994a00f000000000000220020ce6e751274836ff59622a0d1e07f8831d80bd6730bd48581398bfadd2bb8da9ac0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a87856a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220086288faceab47461eb2d808e9e9b0cb3ffc24a03c2f18db7198247d38f10e58022031d1c2782a58c8c6ce187d0019eb47a83babdf3040e2caff299ab48f7e12b1fa01483045022100a8771147109e4d3f44a5976c3c3de98732bbb77308d21444dbe0d76faf06480e02200b4e916e850c3d1f918de87bbbbb07843ffea1d4658dfe060b6f9ccd96d34be801475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "3045022100a8771147109e4d3f44a5976c3c3de98732bbb77308d21444dbe0d76faf06480e02200b4e916e850c3d1f918de87bbbbb07843ffea1d4658dfe060b6f9ccd96d34be8" + }, + { + "Name": "commitment tx with two outputs untrimmed (minimum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 4894, + "UseTestHtlcs": true, + "HtlcDescs": [], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80044a010000000000002200202b1b5854183c12d3316565972c4668929d314d81c5dcdbb21cb45fe8a9a8114f4a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994c0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994ad0886a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004830450221009f16ac85d232e4eddb3fcd750a68ebf0b58e3356eaada45d3513ede7e817bf4c02207c2b043b4e5f971261975406cb955219fa56bffe5d834a833694b5abc1ce4cfd01483045022100e784a66b1588575801e237d35e510fd92a81ae3a4a2a1b90c031ad803d07b3f3022021bc5f16501f167607d63b681442da193eb0a76b4b7fd25c2ed4f8b28fd35b9501475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "3045022100e784a66b1588575801e237d35e510fd92a81ae3a4a2a1b90c031ad803d07b3f3022021bc5f16501f167607d63b681442da193eb0a76b4b7fd25c2ed4f8b28fd35b95" + }, + { + "Name": "commitment tx with one output untrimmed (minimum feerate)", + "LocalBalance": 6988000000, + "RemoteBalance": 3000000000, + "FeePerKw": 6216010, + "UseTestHtlcs": true, + "HtlcDescs": [], + "ExpectedCommitmentTxHex": "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b80024a01000000000000220020e9e86e4823faa62e222ebc858a226636856158f07e69898da3b0d1af0ddb3994c0c62d0000000000220020f3394e1e619b0eca1f91be2fb5ab4dfc59ba5b84ebe014ad1d43a564d012994a04004830450221009ad80792e3038fe6968d12ff23e6888a565c3ddd065037f357445f01675d63f3022018384915e5f1f4ae157e15debf4f49b61c8d9d2b073c7d6f97c4a68caa3ed4c1014830450221008fd5dbff02e4b59020d4cd23a3c30d3e287065fda75a0a09b402980adf68ccda022001e0b8b620cd915ddff11f1de32addf23d81d51b90e6841b2cb8dcaf3faa5ecf01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220", + "RemoteSigHex": "30450221008fd5dbff02e4b59020d4cd23a3c30d3e287065fda75a0a09b402980adf68ccda022001e0b8b620cd915ddff11f1de32addf23d81d51b90e6841b2cb8dcaf3faa5ecf" + } +] \ No newline at end of file diff --git a/electrum/tests/regtest.py b/electrum/tests/regtest.py index b42e4b3315ac..7a79baf10ab1 100644 --- a/electrum/tests/regtest.py +++ b/electrum/tests/regtest.py @@ -3,11 +3,17 @@ import unittest import subprocess -class TestLightning(unittest.TestCase): - @staticmethod - def run_shell(args, timeout=30): - process = subprocess.Popen(['electrum/tests/regtest/regtest.sh'] + args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, universal_newlines=True) +class TestLightning(unittest.TestCase): + TEST_ANCHOR_CHANNELS = False + + def run_shell(self, args, timeout=30): + process = subprocess.Popen( + ['electrum/tests/regtest/regtest.sh'] + args, + stderr=subprocess.STDOUT, stdout=subprocess.PIPE, + universal_newlines=True, + env=os.environ.update({'TEST_ANCHOR_CHANNELS': str(self.TEST_ANCHOR_CHANNELS)}), + ) for line in iter(process.stdout.readline, ''): sys.stdout.write(line) sys.stdout.flush() @@ -63,8 +69,16 @@ def test_breach_with_spent_htlc(self): self.run_shell(['breach_with_spent_htlc']) +class TestLightningABAnchors(TestLightningAB): + TEST_ANCHOR_CHANNELS = True + + class TestLightningABC(TestLightning): agents = ['alice', 'bob', 'carol'] def test_watchtower(self): self.run_shell(['watchtower']) + + +class TestLightningABCAnchors(TestLightningABC): + TEST_ANCHOR_CHANNELS = True diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index b3b366f5272d..58f62b1a6d75 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -72,10 +72,12 @@ if [[ $1 == "new_block" ]]; then fi if [[ $1 == "init" ]]; then + echo "testing anchor channels: $TEST_ANCHOR_CHANNELS" echo "initializing $2" rm -rf /tmp/$2/ agent="./run_electrum --regtest -D /tmp/$2" $agent create --offline > /dev/null + $agent setconfig --offline enable_anchor_channels $TEST_ANCHOR_CHANNELS $agent setconfig --offline log_to_file True $agent setconfig --offline use_gossip True $agent setconfig --offline server 127.0.0.1:51001:t @@ -83,6 +85,9 @@ if [[ $1 == "init" ]]; then # alice is funded, bob is listening if [[ $2 == "bob" ]]; then $bob setconfig --offline lightning_listen localhost:9735 + echo "funding $2" + # add some funds to bob as anchor reserves + $bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 0.1 else echo "funding $2" $bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1 @@ -128,7 +133,7 @@ if [[ $1 == "breach" ]]; then new_blocks 1 wait_until_channel_closed bob new_blocks 1 - wait_for_balance bob 0.14 + wait_for_balance bob 0.24 $bob getbalance fi @@ -154,7 +159,8 @@ if [[ $1 == "backup" ]]; then $alice request_force_close $channel1 echo "request force close $channel2" $alice request_force_close $channel2 - wait_for_balance alice 0.998 + new_blocks 1 + wait_for_balance alice 0.997 fi @@ -257,7 +263,7 @@ if [[ $1 == "breach_with_unspent_htlc" ]]; then fi echo "alice breaches with old ctx" $bitcoin_cli sendrawtransaction $ctx - wait_for_balance bob 0.14 + wait_for_balance bob 0.24 fi @@ -305,14 +311,22 @@ if [[ $1 == "breach_with_spent_htlc" ]]; then $alice load_wallet -w /tmp/alice/regtest/wallets/toxic_wallet # wait until alice has spent both ctx outputs echo "alice spends to_local and htlc outputs" - wait_until_spent $ctx_id 0 - wait_until_spent $ctx_id 1 + if [ $TEST_ANCHOR_CHANNELS = True ] ; then + # to_local_anchor/to_remote_anchor: 0 and 1 (both are present due to untrimmed htlcs) + # htlc: 2, to_local: 3 + wait_until_spent $ctx_id 2 + wait_until_spent $ctx_id 3 + else + # htlc: 0, to_local: 1 + wait_until_spent $ctx_id 0 + wait_until_spent $ctx_id 1 + fi new_blocks 1 echo "bob comes back" $bob daemon -d sleep 1 $bob load_wallet - wait_for_balance bob 0.039 + wait_for_balance bob 0.139 $bob getbalance fi @@ -354,7 +368,12 @@ if [[ $1 == "watchtower" ]]; then ctx_id=$($bitcoin_cli sendrawtransaction $ctx) echo "alice breaches with old ctx:" $ctx_id echo "watchtower publishes justice transaction" - wait_until_spent $ctx_id 1 # alice's to_local gets punished immediately + if [ $TEST_ANCHOR_CHANNELS = True ] ; then + output_index=3 + else + output_index=1 + fi + wait_until_spent $ctx_id $output_index # alice's to_local gets punished fi if [[ $1 == "unixsockets" ]]; then diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py index eb6230e4ee13..afcb175c9074 100644 --- a/electrum/tests/test_lnchannel.py +++ b/electrum/tests/test_lnchannel.py @@ -33,7 +33,7 @@ from electrum import lnchannel from electrum import lnutil from electrum import bip32 as bip32_utils -from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED +from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED, effective_htlc_tx_weight from electrum.logging import console_stderr_handler from electrum.lnchannel import ChannelState from electrum.json_db import StoredDict @@ -41,14 +41,13 @@ from . import ElectrumTestCase - one_bitcoin_in_msat = bitcoin.COIN * 1000 def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, - r_csv): + r_csv, anchor_outputs): assert local_amount > 0 assert remote_amount > 0 channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index) @@ -107,6 +106,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator, 'fail_htlc_reasons': {}, 'unfulfilled_htlcs': {}, 'revocation_store': {}, + 'has_anchors': anchor_outputs, } return StoredDict(state, None, []) @@ -119,7 +119,8 @@ def bip32(sequence): def create_test_channels(*, feerate=6000, local_msat=None, remote_msat=None, alice_name="alice", bob_name="bob", - alice_pubkey=b"\x01"*33, bob_pubkey=b"\x02"*33, random_seed=None): + alice_pubkey=b"\x01"*33, bob_pubkey=b"\x02"*33, random_seed=None, + anchor_outputs=False): if random_seed is None: # needed for deterministic randomness random_seed = os.urandom(32) random_gen = PRNG(random_seed) @@ -151,7 +152,7 @@ def create_test_channels(*, feerate=6000, local_msat=None, remote_msat=None, funding_txid, funding_index, funding_sat, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, None, bob_first, other_node_id=bob_pubkey, l_dust=200, r_dust=1300, - l_csv=5, r_csv=4 + l_csv=5, r_csv=4, anchor_outputs=anchor_outputs ), name=f"{alice_name}->{bob_name}", initial_feerate=feerate), @@ -160,7 +161,7 @@ def create_test_channels(*, feerate=6000, local_msat=None, remote_msat=None, funding_txid, funding_index, funding_sat, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, None, alice_first, other_node_id=alice_pubkey, l_dust=1300, r_dust=200, - l_csv=4, r_csv=5 + l_csv=4, r_csv=5, anchor_outputs=anchor_outputs ), name=f"{bob_name}->{alice_name}", initial_feerate=feerate) @@ -205,8 +206,10 @@ class TestFee(ElectrumTestCase): def test_fee(self): alice_channel, bob_channel = create_test_channels(feerate=253, local_msat=10000000000, - remote_msat=5000000000) - self.assertIn(9999817, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()]) + remote_msat=5000000000, anchor_outputs=self.TEST_ANCHOR_CHANNELS) + expected_value = 9999056 if self.TEST_ANCHOR_CHANNELS else 9999817 + self.assertIn(expected_value, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()]) + class TestChannel(ElectrumTestCase): maxDiff = 999 @@ -218,6 +221,9 @@ def assertOutputExistsByValue(self, tx, amt_sat): else: self.assertFalse() + def assertNumberNonAnchorOutputs(self, number, tx): + self.assertEqual(number, len(tx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + @classmethod def setUpClass(cls): super().setUpClass() @@ -228,15 +234,15 @@ def setUp(self): # Create a test channel which will be used for the duration of this # unittest. The channel will be funded evenly with Alice having 5 BTC, # and Bob having 5 BTC. - self.alice_channel, self.bob_channel = create_test_channels() + self.alice_channel, self.bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) self.paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(self.paymentPreimage) self.htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : one_bitcoin_in_msat, - 'cltv_expiry' : 5, - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': one_bitcoin_in_msat, + 'cltv_expiry': 5, + 'timestamp': 0, } # First Alice adds the outgoing HTLC to her local channel's state @@ -258,40 +264,60 @@ def test_concurrent_reversed_payment(self): self.bob_channel.add_htlc(self.htlc_dict) self.alice_channel.receive_htlc(self.htlc_dict) - self.assertEqual(len(self.alice_channel.get_latest_commitment(LOCAL).outputs()), 2) - self.assertEqual(len(self.alice_channel.get_next_commitment(LOCAL).outputs()), 3) - self.assertEqual(len(self.alice_channel.get_latest_commitment(REMOTE).outputs()), 2) - self.assertEqual(len(self.alice_channel.get_next_commitment(REMOTE).outputs()), 3) + self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(LOCAL)) + self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(LOCAL)) + self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(REMOTE)) + self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(REMOTE)) self.alice_channel.receive_new_commitment(*self.bob_channel.sign_next_commitment()) - self.assertEqual(len(self.alice_channel.get_latest_commitment(LOCAL).outputs()), 3) - self.assertEqual(len(self.alice_channel.get_next_commitment(LOCAL).outputs()), 3) - self.assertEqual(len(self.alice_channel.get_latest_commitment(REMOTE).outputs()), 2) - self.assertEqual(len(self.alice_channel.get_next_commitment(REMOTE).outputs()), 3) + self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_latest_commitment(LOCAL)) + self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(LOCAL)) + self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(REMOTE)) + self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(REMOTE)) self.alice_channel.revoke_current_commitment() - self.assertEqual(len(self.alice_channel.get_latest_commitment(LOCAL).outputs()), 3) - self.assertEqual(len(self.alice_channel.get_next_commitment(LOCAL).outputs()), 3) - self.assertEqual(len(self.alice_channel.get_latest_commitment(REMOTE).outputs()), 2) - self.assertEqual(len(self.alice_channel.get_next_commitment(REMOTE).outputs()), 4) + self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_latest_commitment(LOCAL)) + self.assertNumberNonAnchorOutputs(3, self.alice_channel.get_next_commitment(LOCAL)) + self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(REMOTE)) + self.assertNumberNonAnchorOutputs(4, self.alice_channel.get_next_commitment(REMOTE)) def test_SimpleAddSettleWorkflow(self): alice_channel, bob_channel = self.alice_channel, self.bob_channel htlc = self.htlc + # Starting point: alice has sent an update_add_htlc message to bob + # but the htlc is not yet committed to alice_out = alice_channel.get_latest_commitment(LOCAL).outputs() - short_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 42] - long_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 62] - self.assertLess(alice_out[long_idx].value, 5 * 10**8, alice_out) - self.assertEqual(alice_out[short_idx].value, 5 * 10**8, alice_out) + if not alice_channel.has_anchors(): + # ctx outputs are ordered by increasing amounts + low_amt_idx = 0 + assert len(alice_out[low_amt_idx].address) == 62 # p2wsh + high_amt_idx = 1 + assert len(alice_out[high_amt_idx].address) == 42 # p2wpkh + else: + # using anchor outputs, all outputs are p2wsh + low_amt_idx = 2 + assert len(alice_out[low_amt_idx].address) == 62 + high_amt_idx = 3 + assert len(alice_out[high_amt_idx].address) == 62 + self.assertLess(alice_out[low_amt_idx].value, 5 * 10**8, alice_out) + self.assertEqual(alice_out[high_amt_idx].value, 5 * 10**8, alice_out) alice_out = alice_channel.get_latest_commitment(REMOTE).outputs() - short_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 42] - long_idx, = [idx for idx, x in enumerate(alice_out) if len(x.address) == 62] - self.assertLess(alice_out[short_idx].value, 5 * 10**8) - self.assertEqual(alice_out[long_idx].value, 5 * 10**8) + if not alice_channel.has_anchors(): + low_amt_idx = 0 + assert len(alice_out[low_amt_idx].address) == 42 + high_amt_idx = 1 + assert len(alice_out[high_amt_idx].address) == 62 + else: + low_amt_idx = 2 + assert len(alice_out[low_amt_idx].address) == 62 + high_amt_idx = 3 + assert len(alice_out[high_amt_idx].address) == 62 + self.assertLess(alice_out[low_amt_idx].value, 5 * 10**8) + self.assertEqual(alice_out[high_amt_idx].value, 5 * 10**8) self.assertTrue(alice_channel.signature_fits(alice_channel.get_latest_commitment(LOCAL))) @@ -336,7 +362,7 @@ def test_SimpleAddSettleWorkflow(self): self.assertTrue(bob_channel.signature_fits(bob_channel.get_latest_commitment(LOCAL))) self.assertEqual(bob_channel.get_oldest_unrevoked_ctn(REMOTE), 0) - self.assertEqual(bob_channel.included_htlcs(LOCAL, RECEIVED, 1), [htlc])# + self.assertEqual(bob_channel.included_htlcs(LOCAL, RECEIVED, 1), [htlc]) self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 0), []) self.assertEqual(alice_channel.included_htlcs(REMOTE, RECEIVED, 1), [htlc]) @@ -364,10 +390,10 @@ def test_SimpleAddSettleWorkflow(self): self.assertTrue(alice_channel.signature_fits(alice_channel.get_latest_commitment(LOCAL))) # so far: Alice added htlc, Alice signed. - self.assertEqual(len(alice_channel.get_latest_commitment(LOCAL).outputs()), 2) - self.assertEqual(len(alice_channel.get_next_commitment(LOCAL).outputs()), 2) - self.assertEqual(len(alice_channel.get_oldest_unrevoked_commitment(REMOTE).outputs()), 2) - self.assertEqual(len(alice_channel.get_latest_commitment(REMOTE).outputs()), 3) + self.assertNumberNonAnchorOutputs(2, alice_channel.get_latest_commitment(LOCAL)) + self.assertNumberNonAnchorOutputs(2, alice_channel.get_next_commitment(LOCAL)) + self.assertNumberNonAnchorOutputs(2, alice_channel.get_oldest_unrevoked_commitment(REMOTE)) + self.assertNumberNonAnchorOutputs(3, alice_channel.get_latest_commitment(REMOTE)) # Alice then processes this revocation, sending her own revocation for # her prior commitment transaction. Alice shouldn't have any HTLCs to @@ -376,21 +402,21 @@ def test_SimpleAddSettleWorkflow(self): self.assertTrue(alice_channel.signature_fits(alice_channel.get_latest_commitment(LOCAL))) - self.assertEqual(len(alice_channel.get_latest_commitment(LOCAL).outputs()), 2) - self.assertEqual(len(alice_channel.get_latest_commitment(REMOTE).outputs()), 3) - self.assertEqual(len(alice_channel.force_close_tx().outputs()), 2) + self.assertNumberNonAnchorOutputs(2, alice_channel.get_latest_commitment(LOCAL)) + self.assertNumberNonAnchorOutputs(3, alice_channel.get_latest_commitment(REMOTE)) + self.assertNumberNonAnchorOutputs(2, alice_channel.force_close_tx()) self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1) self.assertEqual(alice_channel.get_next_commitment(LOCAL).outputs(), bob_channel.get_latest_commitment(REMOTE).outputs()) # Alice then processes bob's signature, and since she just received - # the revocation, she expect this signature to cover everything up to + # the revocation, she expects this signature to cover everything up to # the point where she sent her signature, including the HTLC. alice_channel.receive_new_commitment(bobSig, bobHtlcSigs) - self.assertEqual(len(alice_channel.get_latest_commitment(REMOTE).outputs()), 3) - self.assertEqual(len(alice_channel.force_close_tx().outputs()), 3) + self.assertNumberNonAnchorOutputs(3, alice_channel.get_latest_commitment(REMOTE)) + self.assertNumberNonAnchorOutputs(3, alice_channel.force_close_tx()) self.assertEqual(len(alice_channel.hm.log[LOCAL]['adds']), 1) @@ -428,8 +454,8 @@ def test_SimpleAddSettleWorkflow(self): # them should be exactly the amount of the HTLC. alice_ctx = alice_channel.get_next_commitment(LOCAL) bob_ctx = bob_channel.get_next_commitment(LOCAL) - self.assertEqual(len(alice_ctx.outputs()), 3, "alice should have three commitment outputs, instead have %s"% len(alice_ctx.outputs())) - self.assertEqual(len(bob_ctx.outputs()), 3, "bob should have three commitment outputs, instead have %s"% len(bob_ctx.outputs())) + self.assertNumberNonAnchorOutputs(3, alice_ctx) + self.assertNumberNonAnchorOutputs(3, bob_ctx) self.assertOutputExistsByValue(alice_ctx, htlc.amount_msat // 1000) self.assertOutputExistsByValue(bob_ctx, htlc.amount_msat // 1000) @@ -477,7 +503,7 @@ def test_SimpleAddSettleWorkflow(self): aliceRevocation2 = alice_channel.revoke_current_commitment() aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment() self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures") - self.assertEqual(len(bob_channel.get_latest_commitment(LOCAL).outputs()), 3) + self.assertNumberNonAnchorOutputs(3, bob_channel.get_latest_commitment(LOCAL)) bob_channel.receive_revocation(aliceRevocation2) bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2) @@ -531,7 +557,6 @@ def test_SimpleAddSettleWorkflow(self): self.assertEqual(bob_channel.total_msat(RECEIVED), one_bitcoin_in_msat, "bob satoshis received incorrect") self.assertEqual(bob_channel.total_msat(SENT), 5 * one_bitcoin_in_msat, "bob satoshis sent incorrect") - def alice_to_bob_fee_update(self, fee=1111): aoldctx = self.alice_channel.get_next_commitment(REMOTE).outputs() self.alice_channel.update_fee(fee, True) @@ -635,30 +660,34 @@ def test_AddHTLCNegativeBalance(self): self.assertIn('Not enough local balance', cm.exception.args[0]) +class TestChannelAnchors(TestChannel): + TEST_ANCHOR_CHANNELS = True + + class TestAvailableToSpend(ElectrumTestCase): def test_DesyncHTLCs(self): - alice_channel, bob_channel = create_test_channels() - self.assertEqual(499986152000, alice_channel.available_to_spend(LOCAL)) + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) + self.assertEqual(499986152000 if not alice_channel.has_anchors() else 499981351340, alice_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(paymentPreimage) htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : one_bitcoin_in_msat * 41 // 10, - 'cltv_expiry' : 5, - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': one_bitcoin_in_msat * 41 // 10, + 'cltv_expiry': 5, + 'timestamp': 0, } alice_idx = alice_channel.add_htlc(htlc_dict).htlc_id bob_idx = bob_channel.receive_htlc(htlc_dict).htlc_id - self.assertEqual(89984088000, alice_channel.available_to_spend(LOCAL)) + self.assertEqual(89984088000 if not alice_channel.has_anchors() else 89979287340, alice_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) force_state_transition(alice_channel, bob_channel) bob_channel.fail_htlc(bob_idx) alice_channel.receive_fail_htlc(alice_idx, error_bytes=None) - self.assertEqual(89984088000, alice_channel.available_to_spend(LOCAL)) + self.assertEqual(89984088000 if not alice_channel.has_anchors() else 89979287340, alice_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) # Alice now has gotten all her original balance (5 BTC) back, however, # adding a new HTLC at this point SHOULD fail, since if she adds the @@ -668,24 +697,28 @@ def test_DesyncHTLCs(self): # We try adding an HTLC of value 1 BTC, which should fail because the # balance is unavailable. htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : one_bitcoin_in_msat, - 'cltv_expiry' : 5, - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': one_bitcoin_in_msat, + 'cltv_expiry': 5, + 'timestamp': 0, } with self.assertRaises(lnutil.PaymentFailure): alice_channel.add_htlc(htlc_dict) # Now do a state transition, which will ACK the FailHTLC, making Alice # able to add the new HTLC. force_state_transition(alice_channel, bob_channel) - self.assertEqual(499986152000, alice_channel.available_to_spend(LOCAL)) + self.assertEqual(499986152000 if not alice_channel.has_anchors() else 499981351340, alice_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) alice_channel.add_htlc(htlc_dict) +class TestAvailableToSpendAnchors(TestAvailableToSpend): + TEST_ANCHOR_CHANNELS = True + + class TestChanReserve(ElectrumTestCase): def setUp(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=False) alice_min_reserve = int(.5 * one_bitcoin_in_msat // 1000) # We set Bob's channel reserve to a value that is larger than # his current balance in the channel. This will ensure that @@ -715,10 +748,10 @@ def test_part1(self): paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(paymentPreimage) htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : int(.5 * one_bitcoin_in_msat), - 'cltv_expiry' : 5, - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': int(.5 * one_bitcoin_in_msat), + 'cltv_expiry': 5, + 'timestamp': 0, } self.alice_channel.add_htlc(htlc_dict) self.bob_channel.receive_htlc(htlc_dict) @@ -754,9 +787,9 @@ def part2(self): # Alice: 1.5 # Bob: 9.5 htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : int(3.5 * one_bitcoin_in_msat), - 'cltv_expiry' : 5, + 'payment_hash': paymentHash, + 'amount_msat': int(3.5 * one_bitcoin_in_msat), + 'cltv_expiry': 5, } self.alice_channel.add_htlc(htlc_dict) self.bob_channel.receive_htlc(htlc_dict) @@ -778,10 +811,10 @@ def part3(self): paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(paymentPreimage) htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : int(2 * one_bitcoin_in_msat), - 'cltv_expiry' : 5, - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': int(2 * one_bitcoin_in_msat), + 'cltv_expiry': 5, + 'timestamp': 0, } alice_idx = self.alice_channel.add_htlc(htlc_dict).htlc_id bob_idx = self.bob_channel.receive_htlc(htlc_dict).htlc_id @@ -812,40 +845,72 @@ def check_bals(self, amt1, amt2): self.assertEqual(self.alice_channel.available_to_spend(REMOTE), amt2) self.assertEqual(self.bob_channel.available_to_spend(LOCAL), amt2) + +class TestChanReserveAnchors(TestChanReserve): + TEST_ANCHOR_CHANNELS = True + + class TestDust(ElectrumTestCase): def test_DustLimit(self): - alice_channel, bob_channel = create_test_channels() + """Test that addition of an HTLC below the dust limit changes the balances.""" + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) + dust_limit_alice = alice_channel.config[LOCAL].dust_limit_sat + dust_limit_bob = bob_channel.config[LOCAL].dust_limit_sat + self.assertLess(dust_limit_alice, dust_limit_bob) + bob_ctx = bob_channel.get_latest_commitment(LOCAL) + bobs_original_outputs = [x.value for x in bob_ctx.outputs()] paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(paymentPreimage) fee_per_kw = alice_channel.get_next_feerate(LOCAL) - self.assertEqual(fee_per_kw, 6000) - htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000) - self.assertEqual(htlcAmt, 4478) + success_weight = effective_htlc_tx_weight(success=True, has_anchors=self.TEST_ANCHOR_CHANNELS) + # we put a single sat less into the htlc than bob can afford + # to pay for his htlc success transaction + below_dust_for_bob = dust_limit_bob - 1 + htlc_amt = below_dust_for_bob + success_weight * (fee_per_kw // 1000) htlc = { - 'payment_hash' : paymentHash, - 'amount_msat' : 1000 * htlcAmt, - 'cltv_expiry' : 5, # also in create_test_channels - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': 1000 * htlc_amt, + 'cltv_expiry': 5, # consistent with channel policy + 'timestamp': 0, } - old_values = [x.value for x in bob_channel.get_latest_commitment(LOCAL).outputs()] - aliceHtlcIndex = alice_channel.add_htlc(htlc).htlc_id - bobHtlcIndex = bob_channel.receive_htlc(htlc).htlc_id + # add the htlc + alice_htlc_id = alice_channel.add_htlc(htlc).htlc_id + bob_htlc_id = bob_channel.receive_htlc(htlc).htlc_id force_state_transition(alice_channel, bob_channel) alice_ctx = alice_channel.get_latest_commitment(LOCAL) bob_ctx = bob_channel.get_latest_commitment(LOCAL) - new_values = [x.value for x in bob_ctx.outputs()] - self.assertNotEqual(old_values, new_values) - self.assertEqual(len(alice_ctx.outputs()), 3) - self.assertEqual(len(bob_ctx.outputs()), 2) - default_fee = calc_static_fee(0) - self.assertEqual(bob_channel.get_next_fee(LOCAL), default_fee + htlcAmt) - bob_channel.settle_htlc(paymentPreimage, bobHtlcIndex) - alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex) + bobs_second_outputs = [x.value for x in bob_ctx.outputs()] + self.assertNotEqual(bobs_original_outputs, bobs_second_outputs) + # the htlc appears as an output in alice's ctx, as she has a lower + # dust limit (also because her timeout tx costs less) + self.assertEqual(3, len(alice_ctx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + # htlc in bob's case goes to miner fees + self.assertEqual(2, len(bob_ctx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(htlc_amt, sum(bobs_original_outputs) - sum(bobs_second_outputs)) + empty_ctx_fee = lnutil.calc_fees_for_commitment_tx( + num_htlcs=0, feerate=fee_per_kw, is_local_initiator=True, + round_to_sat=True, has_anchors=self.TEST_ANCHOR_CHANNELS)[LOCAL] // 1000 + self.assertEqual(empty_ctx_fee + htlc_amt, bob_channel.get_next_fee(LOCAL)) + + bob_channel.settle_htlc(paymentPreimage, bob_htlc_id) + alice_channel.receive_htlc_settle(paymentPreimage, alice_htlc_id) force_state_transition(bob_channel, alice_channel) - self.assertEqual(len(alice_channel.get_next_commitment(LOCAL).outputs()), 2) - self.assertEqual(alice_channel.total_msat(SENT) // 1000, htlcAmt) + bob_ctx = bob_channel.get_latest_commitment(LOCAL) + bobs_third_outputs = [x.value for x in bob_ctx.outputs()] + # htlc is added back into the balance + self.assertEqual(sum(bobs_original_outputs), sum(bobs_third_outputs)) + # balance shifts in bob's direction after settlement + self.assertEqual(htlc_amt, bobs_third_outputs[1 + (2 if self.TEST_ANCHOR_CHANNELS else 0)] - bobs_original_outputs[1 + (2 if self.TEST_ANCHOR_CHANNELS else 0)]) + self.assertEqual(2, len(alice_channel.get_next_commitment(LOCAL).outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(2, len(bob_channel.get_next_commitment(LOCAL).outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(htlc_amt, alice_channel.total_msat(SENT) // 1000) + + +class TestDustAnchors(TestDust): + TEST_ANCHOR_CHANNELS = True + def force_state_transition(chanA, chanB): chanB.receive_new_commitment(*chanA.sign_next_commitment()) @@ -854,12 +919,3 @@ def force_state_transition(chanA, chanB): chanA.receive_revocation(rev) chanA.receive_new_commitment(bob_sig, bob_htlc_sigs) chanB.receive_revocation(chanA.revoke_current_commitment()) - -# calcStaticFee calculates appropriate fees for commitment transactions. This -# function provides a simple way to allow test balance assertions to take fee -# calculations into account. -def calc_static_fee(numHTLCs): - commitWeight = 724 - htlcWeight = 172 - feePerKw = 24//4 * 1000 - return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000 diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 772a3af72220..bd108464c952 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -41,8 +41,10 @@ from .test_lnchannel import create_test_channels from .test_bitcoin import needs_test_with_all_chacha20_implementations + from . import TestCaseForTestnet + def keypair(): priv = ECPrivkey.generate_random_key().get_secret_bytes() k1 = Keypair( @@ -113,13 +115,22 @@ def is_lightning_backup(self): def is_mine(self, addr): return True + def get_new_sweep_address_for_channel(self): + return None + + def is_watching_only(self): + return False + + def can_sign_without_user_interaction_if_have_password(self): + return True + class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): MPP_EXPIRY = 2 # HTLC timestamps are cast to int, so this cannot be 1 TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 0 INITIAL_TRAMPOLINE_FEE_LEVEL = 0 - def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue, name): + def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue, name, has_anchors): self.name = name Logger.__init__(self) NetworkRetryManager.__init__(self, max_retry_delay_normal=1, init_retry_delay_normal=1) @@ -138,6 +149,9 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.features |= LnFeatures.VAR_ONION_OPT self.features |= LnFeatures.PAYMENT_SECRET_OPT self.features |= LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT + self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT + self.config = {'enable_anchor_channels': has_anchors} + self.maybe_enable_anchors_store_password(None) self.pending_payments = defaultdict(asyncio.Future) for chan in chans: chan.lnworker = self @@ -154,6 +168,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.preimages = {} self.stopping_soon = False self.downstream_htlc_to_upstream_peer_map = {} + self.wallet_password = None self.logger.info(f"created LNWallet[{name}] with nodeID={local_keypair.pubkey.hex()}") @@ -243,6 +258,8 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln _decode_channel_update_msg = LNWallet._decode_channel_update_msg _handle_chanupd_from_failed_htlc = LNWallet._handle_chanupd_from_failed_htlc _on_maybe_forwarded_htlc_resolved = LNWallet._on_maybe_forwarded_htlc_resolved + maybe_enable_anchors_store_password = LNWallet.maybe_enable_anchors_store_password + has_anchor_channels = LNWallet.has_anchor_channels class MockTransport: @@ -357,8 +374,8 @@ def prepare_peers(self, alice_channel: Channel, bob_channel: Channel): bob_channel.node_id = k1.pubkey t1, t2 = transport_pair(k1, k2, alice_channel.name, bob_channel.name) q1, q2 = asyncio.Queue(), asyncio.Queue() - w1 = MockLNWallet(local_keypair=k1, chans=[alice_channel], tx_queue=q1, name=bob_channel.name) - w2 = MockLNWallet(local_keypair=k2, chans=[bob_channel], tx_queue=q2, name=alice_channel.name) + w1 = MockLNWallet(local_keypair=k1, chans=[alice_channel], tx_queue=q1, name=bob_channel.name, has_anchors=self.TEST_ANCHOR_CHANNELS) + w2 = MockLNWallet(local_keypair=k2, chans=[bob_channel], tx_queue=q2, name=alice_channel.name, has_anchors=self.TEST_ANCHOR_CHANNELS) self._lnworkers_created.extend([w1, w2]) p1 = PeerInTests(w1, k2.pubkey, t1) p2 = PeerInTests(w2, k1.pubkey, t2) @@ -383,6 +400,7 @@ def prepare_chans_and_peers_in_square(self, funds_distribution: Dict[str, Tuple[ alice_pubkey=key_a.pubkey, bob_pubkey=key_b.pubkey, local_msat=local_balance, remote_msat=remote_balance, + anchor_outputs=self.TEST_ANCHOR_CHANNELS ) local_balance, remote_balance = funds_distribution.get('ac') or (None, None) chan_ac, chan_ca = create_test_channels( @@ -390,6 +408,7 @@ def prepare_chans_and_peers_in_square(self, funds_distribution: Dict[str, Tuple[ alice_pubkey=key_a.pubkey, bob_pubkey=key_c.pubkey, local_msat=local_balance, remote_msat=remote_balance, + anchor_outputs=self.TEST_ANCHOR_CHANNELS ) local_balance, remote_balance = funds_distribution.get('bd') or (None, None) chan_bd, chan_db = create_test_channels( @@ -397,6 +416,7 @@ def prepare_chans_and_peers_in_square(self, funds_distribution: Dict[str, Tuple[ alice_pubkey=key_b.pubkey, bob_pubkey=key_d.pubkey, local_msat=local_balance, remote_msat=remote_balance, + anchor_outputs=self.TEST_ANCHOR_CHANNELS ) local_balance, remote_balance = funds_distribution.get('cd') or (None, None) chan_cd, chan_dc = create_test_channels( @@ -404,16 +424,17 @@ def prepare_chans_and_peers_in_square(self, funds_distribution: Dict[str, Tuple[ alice_pubkey=key_c.pubkey, bob_pubkey=key_d.pubkey, local_msat=local_balance, remote_msat=remote_balance, + anchor_outputs=self.TEST_ANCHOR_CHANNELS ) trans_ab, trans_ba = transport_pair(key_a, key_b, chan_ab.name, chan_ba.name) trans_ac, trans_ca = transport_pair(key_a, key_c, chan_ac.name, chan_ca.name) trans_bd, trans_db = transport_pair(key_b, key_d, chan_bd.name, chan_db.name) trans_cd, trans_dc = transport_pair(key_c, key_d, chan_cd.name, chan_dc.name) txq_a, txq_b, txq_c, txq_d = [asyncio.Queue() for i in range(4)] - w_a = MockLNWallet(local_keypair=key_a, chans=[chan_ab, chan_ac], tx_queue=txq_a, name="alice") - w_b = MockLNWallet(local_keypair=key_b, chans=[chan_ba, chan_bd], tx_queue=txq_b, name="bob") - w_c = MockLNWallet(local_keypair=key_c, chans=[chan_ca, chan_cd], tx_queue=txq_c, name="carol") - w_d = MockLNWallet(local_keypair=key_d, chans=[chan_db, chan_dc], tx_queue=txq_d, name="dave") + w_a = MockLNWallet(local_keypair=key_a, chans=[chan_ab, chan_ac], tx_queue=txq_a, name="alice", has_anchors=self.TEST_ANCHOR_CHANNELS) + w_b = MockLNWallet(local_keypair=key_b, chans=[chan_ba, chan_bd], tx_queue=txq_b, name="bob", has_anchors=self.TEST_ANCHOR_CHANNELS) + w_c = MockLNWallet(local_keypair=key_c, chans=[chan_ca, chan_cd], tx_queue=txq_c, name="carol", has_anchors=self.TEST_ANCHOR_CHANNELS) + w_d = MockLNWallet(local_keypair=key_d, chans=[chan_db, chan_dc], tx_queue=txq_d, name="dave", has_anchors=self.TEST_ANCHOR_CHANNELS) self._lnworkers_created.extend([w_a, w_b, w_c, w_d]) peer_ab = PeerInTests(w_a, key_b.pubkey, trans_ab) peer_ac = PeerInTests(w_a, key_c.pubkey, trans_ac) @@ -525,7 +546,7 @@ async def prepare_invoice( return lnaddr2, invoice def test_reestablish(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) for chan in (alice_channel, bob_channel): chan.peer_state = PeerState.DISCONNECTED @@ -545,8 +566,8 @@ async def f(): @needs_test_with_all_chacha20_implementations def test_reestablish_with_old_state(self): random_seed = os.urandom(32) - alice_channel, bob_channel = create_test_channels(random_seed=random_seed) - alice_channel_0, bob_channel_0 = create_test_channels(random_seed=random_seed) # these are identical + alice_channel, bob_channel = create_test_channels(random_seed=random_seed, anchor_outputs=self.TEST_ANCHOR_CHANNELS) + alice_channel_0, bob_channel_0 = create_test_channels(random_seed=random_seed, anchor_outputs=self.TEST_ANCHOR_CHANNELS) # these are identical p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) lnaddr, pay_req = run(self.prepare_invoice(w2)) async def pay(): @@ -580,7 +601,7 @@ async def f(): @needs_test_with_all_chacha20_implementations def test_payment(self): """Alice pays Bob a single HTLC via direct channel.""" - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def pay(lnaddr, pay_req): self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr.paymenthash)) @@ -609,7 +630,7 @@ def test_payment_race(self): before sending 'commitment_signed'. Neither party should fulfill the respective HTLCs until those are irrevocably committed to. """ - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def pay(): await asyncio.wait_for(p1.initialized, 1) @@ -673,10 +694,8 @@ async def f(): with self.assertRaises(PaymentDone): run(f()) - #@unittest.skip("too expensive") - #@needs_test_with_all_chacha20_implementations def test_payments_stresstest(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) alice_init_balance_msat = alice_channel.balance(HTLCOwner.LOCAL) bob_init_balance_msat = bob_channel.balance(HTLCOwner.LOCAL) @@ -1003,7 +1022,7 @@ async def f(): @needs_test_with_all_chacha20_implementations def test_close(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) w1.network.config.set_key('dynamic_fees', False) w2.network.config.set_key('dynamic_fees', False) @@ -1037,7 +1056,7 @@ async def f(): @needs_test_with_all_chacha20_implementations def test_close_upfront_shutdown_script(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) # create upfront shutdown script for bob, alice doesn't use upfront # shutdown script @@ -1106,7 +1125,7 @@ async def main_loop(peer): run(test()) def test_channel_usage_after_closing(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel) lnaddr, pay_req = run(self.prepare_invoice(w2)) @@ -1142,7 +1161,7 @@ async def f(): @needs_test_with_all_chacha20_implementations def test_sending_weird_messages_that_should_be_ignored(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def send_weird_messages(): @@ -1173,7 +1192,7 @@ async def f(): @needs_test_with_all_chacha20_implementations def test_sending_weird_messages__unknown_even_type(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def send_weird_messages(): @@ -1202,7 +1221,7 @@ async def f(): @needs_test_with_all_chacha20_implementations def test_sending_weird_messages__known_msg_with_insufficient_length(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def send_weird_messages(): @@ -1230,5 +1249,9 @@ async def f(): self.assertTrue(isinstance(failing_task.exception(), lnmsg.UnexpectedEndOfStream)) +class TestPeerAnchors(TestCaseForTestnet): + TEST_ANCHOR_CHANNELS = True + + def run(coro): return asyncio.run_coroutine_threadsafe(coro, loop=asyncio.get_event_loop()).result() diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 116cbe8cd6c8..52560028070a 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -1,5 +1,7 @@ +import os import unittest import json +from typing import Dict, List from electrum import bitcoin from electrum.json_db import StoredDict @@ -9,14 +11,18 @@ derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret, get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, - ln_compare_features, IncompatibleLightningFeatures) + ln_compare_features, IncompatibleLightningFeatures, offered_htlc_trim_threshold_sat, + received_htlc_trim_threshold_sat) from electrum.util import bh2u, bfh, MyEncoder -from electrum.transaction import Transaction, PartialTransaction +from electrum.transaction import Transaction, PartialTransaction, Sighash from electrum.lnworker import LNWallet from . import ElectrumTestCase +from .test_bitcoin import disable_ecdsa_r_value_grinding +# test vectors for a single channel +# https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#appendix-c-commitment-and-htlc-transaction-test-vectors funding_tx_id = '8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be' funding_output_index = 0 funding_amount_satoshi = 10000000 @@ -38,6 +44,46 @@ # funding wscript = 5221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae +# anchor test vectors are from https://github.com/lightningnetwork/lightning-rfc/commit/1739746afa3863ca783df9be4b7b0338afb63b49 +anchor_test_vector_path = os.path.join(os.path.dirname(__file__), "anchor-vectors.json") +with open(anchor_test_vector_path) as f: + ANCHOR_TEST_VECTORS = json.load(f) + +# in a commitment transaction with all the below htlcs, the order is different, +# indices 1 and 2 are swapped +TEST_HTLCS = [ + { + 'incoming': True, + 'amount': 1000000, + 'expiry': 500, + 'preimage': "0000000000000000000000000000000000000000000000000000000000000000", + }, + { + 'incoming': True, + 'amount': 2000000, + 'expiry': 501, + 'preimage': "0101010101010101010101010101010101010101010101010101010101010101", + }, + { + 'incoming': False, + 'amount': 2000000, + 'expiry': 502, + 'preimage': "0202020202020202020202020202020202020202020202020202020202020202", + }, + { + 'incoming': False, + 'amount': 3000000, + 'expiry': 503, + 'preimage': "0303030303030303030303030303030303030303030303030303030303030303", + }, + { + 'incoming': True, + 'amount': 4000000, + 'expiry': 504, + 'preimage': "0404040404040404040404040404040404040404040404040404040404040404", + } +] + class TestLNUtil(ElectrumTestCase): def test_shachain_store(self): tests = [ @@ -481,23 +527,23 @@ def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self): htlc_cltv_timeout[2] = 502 htlc_payment_preimage[2] = b"\x02" * 32 - htlc[2] = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[2])) + htlc[2] = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[2]), has_anchors=False) htlc_cltv_timeout[3] = 503 htlc_payment_preimage[3] = b"\x03" * 32 - htlc[3] = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[3])) + htlc[3] = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[3]), has_anchors=False) htlc_cltv_timeout[0] = 500 htlc_payment_preimage[0] = b"\x00" * 32 - htlc[0] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[0]), htlc_cltv_timeout[0]) + htlc[0] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[0]), htlc_cltv_timeout[0], has_anchors=False) htlc_cltv_timeout[1] = 501 htlc_payment_preimage[1] = b"\x01" * 32 - htlc[1] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[1]), htlc_cltv_timeout[1]) + htlc[1] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[1]), htlc_cltv_timeout[1], has_anchors=False) htlc_cltv_timeout[4] = 504 htlc_payment_preimage[4] = b"\x04" * 32 - htlc[4] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[4]), htlc_cltv_timeout[4]) + htlc[4] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[4]), htlc_cltv_timeout[4], has_anchors=False) remote_signature = "304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606" output_commit_tx = "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220" @@ -527,8 +573,10 @@ def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self): local_amount=to_local_msat, remote_amount=to_remote_msat, dust_limit_sat=local_dust_limit_satoshi, - fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=len(htlcs), feerate=local_feerate_per_kw, is_local_initiator=True), - htlcs=htlcs) + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=len(htlcs), feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), + htlcs=htlcs, + has_anchors=False + ) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) self.assertEqual(str(our_commit_tx), output_commit_tx) @@ -555,22 +603,33 @@ def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self): htlc_output_index = {0: 0, 1: 2, 2: 1, 3: 3, 4: 4} for i in range(5): - self.assertEqual(output_htlc_tx[i][1], self.htlc_tx(htlc[i], htlc_output_index[i], + self.assertEqual(output_htlc_tx[i][1], self.htlc_tx( + htlc[i], + htlc_output_index[i], htlcs[i].htlc.amount_msat, htlc_payment_preimage[i], signature_for_output_remote_htlc[i], - output_htlc_tx[i][0], htlc_cltv_timeout[i] if not output_htlc_tx[i][0] else 0, + output_htlc_tx[i][0], + htlc_cltv_timeout[i] if not output_htlc_tx[i][0] else 0, local_feerate_per_kw, - our_commit_tx)) + our_commit_tx, + False, + )) - def htlc_tx(self, htlc, htlc_output_index, amount_msat, htlc_payment_preimage, remote_htlc_sig, success, cltv_timeout, local_feerate_per_kw, our_commit_tx): + def htlc_tx(self, htlc: bytes, htlc_output_index: int, amount_msat: int, + htlc_payment_preimage: bytes, remote_htlc_sig: str, + success: bool, cltv_timeout: int, + local_feerate_per_kw: int, our_commit_tx: PartialTransaction, + has_anchors: bool) -> str: _script, our_htlc_tx_output = make_htlc_tx_output( amount_msat=amount_msat, local_feerate=local_feerate_per_kw, revocationpubkey=local_revocation_pubkey, local_delayedpubkey=local_delayedpubkey, success=success, - to_self_delay=local_delay) + to_self_delay=local_delay, + has_anchors=has_anchors + ) our_htlc_tx_inputs = make_htlc_tx_inputs( htlc_output_txid=our_commit_tx.txid(), htlc_output_index=htlc_output_index, @@ -581,10 +640,16 @@ def htlc_tx(self, htlc, htlc_output_index, amount_msat, htlc_payment_preimage, r inputs=our_htlc_tx_inputs, output=our_htlc_tx_output) + remote_sighash = Sighash.ALL + if has_anchors: + remote_sighash = Sighash.ANYONECANPAY | Sighash.SINGLE + our_htlc_tx.inputs()[0].nsequence = 1 + + our_htlc_tx.inputs()[0].sighash = Sighash.ALL local_sig = our_htlc_tx.sign_txin(0, local_privkey[:-1]) our_htlc_tx_witness = make_htlc_tx_witness( - remotehtlcsig=bfh(remote_htlc_sig) + b"\x01", # 0x01 is SIGHASH_ALL + remotehtlcsig=bfh(remote_htlc_sig) + remote_sighash.to_bytes(1, 'big'), localhtlcsig=bfh(local_sig), payment_preimage=htlc_payment_preimage if success else b'', # will put 00 on witness if timeout witness_script=htlc) @@ -614,8 +679,10 @@ def test_commitment_tx_with_one_output(self): local_amount=to_local_msat, remote_amount=to_remote_msat, dust_limit_sat=local_dust_limit_satoshi, - fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True), - htlcs=[]) + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), + htlcs=[], + has_anchors=False + ) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) self.assertEqual(str(our_commit_tx), output_commit_tx) @@ -643,8 +710,10 @@ def test_commitment_tx_with_fee_greater_than_funder_amount(self): local_amount=to_local_msat, remote_amount=to_remote_msat, dust_limit_sat=local_dust_limit_satoshi, - fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True), - htlcs=[]) + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), + htlcs=[], + has_anchors=False + ) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) self.assertEqual(str(our_commit_tx), output_commit_tx) @@ -710,12 +779,111 @@ def test_simple_commitment_tx_with_no_HTLCs(self): local_amount=to_local_msat, remote_amount=to_remote_msat, dust_limit_sat=local_dust_limit_satoshi, - fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True), - htlcs=[]) + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), + htlcs=[], + has_anchors=False + ) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220' self.assertEqual(str(our_commit_tx), ref_commit_tx_str) + @disable_ecdsa_r_value_grinding + def test_commitment_tx_anchors_test_vectors(self): + # this test is only valid for the original anchor output test vectors (not anchors-zero-fee-htlcs), + # therefore we patch the effective htlc tx weight to result in a finite weight + from electrum import lnutil + effective_htlc_tx_weight_original = lnutil.effective_htlc_tx_weight + def effective_htlc_tx_weight_patched(success: bool, has_anchors: bool): + return lnutil.HTLC_SUCCESS_WEIGHT_ANCHORS if success else lnutil.HTLC_TIMEOUT_WEIGHT_ANCHORS + lnutil.effective_htlc_tx_weight = effective_htlc_tx_weight_patched + + try: + for test_vector in ANCHOR_TEST_VECTORS: + with self.subTest(test_vector['Name']): + to_local_msat = test_vector['LocalBalance'] + to_remote_msat = test_vector['RemoteBalance'] + local_feerate_per_kw = test_vector['FeePerKw'] + ref_commit_tx_str = test_vector['ExpectedCommitmentTxHex'] + remote_signature = test_vector['RemoteSigHex'] + use_test_htlcs = test_vector['UseTestHtlcs'] + htlc_descs = test_vector['HtlcDescs'] # type: List[Dict[str, str]] + + remote_htlcpubkey = remotepubkey + local_htlcpubkey = localpubkey + + # test of the commitment transaction, build htlc outputs first + test_htlcs = {} + if use_test_htlcs: + # only consider htlcs whose sweep transaction creates outputs above dust limit + threshold_sat_received = received_htlc_trim_threshold_sat(dust_limit_sat=local_dust_limit_satoshi, feerate=local_feerate_per_kw, has_anchors=True) + threshold_sat_offered = offered_htlc_trim_threshold_sat(dust_limit_sat=local_dust_limit_satoshi, feerate=local_feerate_per_kw, has_anchors=True) + for test_index, test_htlc in enumerate(TEST_HTLCS): + if test_htlc['incoming']: + htlc_script = make_received_htlc( + local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, + bitcoin.sha256(bfh(test_htlc['preimage'])), test_htlc['expiry'], + has_anchors=True) + else: + htlc_script = make_offered_htlc( + local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, + bitcoin.sha256(bfh(test_htlc['preimage'])), has_anchors=True) + update_add_htlc = UpdateAddHtlc( + amount_msat=test_htlc['amount'], + payment_hash=bitcoin.sha256(bfh(test_htlc['preimage'])), + cltv_expiry=test_htlc['expiry'], + htlc_id=None, + timestamp=0) + # only add htlcs whose spending transaction creates above-dust ouputs + # TODO: should we include this check in make_commitment? + if test_htlc['amount'] // 1000 >= (threshold_sat_received if test_htlc['incoming'] else threshold_sat_offered): + test_htlcs[test_index] = ScriptHtlc(htlc_script, update_add_htlc) + + our_commit_tx = make_commitment( + ctn=commitment_number, + local_funding_pubkey=local_funding_pubkey, + remote_funding_pubkey=remote_funding_pubkey, + remote_payment_pubkey=remote_payment_basepoint, # no key rotation for anchors + funder_payment_basepoint=local_payment_basepoint, + fundee_payment_basepoint=remote_payment_basepoint, + revocation_pubkey=local_revocation_pubkey, + delayed_pubkey=local_delayedpubkey, + to_self_delay=local_delay, + funding_txid=funding_tx_id, + funding_pos=funding_output_index, + funding_sat=funding_amount_satoshi, + local_amount=to_local_msat, + remote_amount=to_remote_msat, + dust_limit_sat=local_dust_limit_satoshi, + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=len(test_htlcs), feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=True), + htlcs=list(test_htlcs.values()), + has_anchors=True + ) + self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) + self.assertEqual(str(our_commit_tx), ref_commit_tx_str) # only works without r value grinding + + # test the transactions spending the htlc outputs + # we need to keep track of the htlc order in order to compare to test vectors + sorted_htlcs = {h[0]: h[1] for h in sorted(test_htlcs.items(), key=lambda x: (x[1].htlc.amount_msat, -x[1].htlc.cltv_expiry))} + if use_test_htlcs: + for output_index, (test_index, htlc) in enumerate(sorted_htlcs.items()): + test_htlc = TEST_HTLCS[test_index] + our_htlc = self.htlc_tx( + htlc=htlc.redeem_script, + htlc_output_index=output_index + 2, # first two are anchors + amount_msat=htlc.htlc.amount_msat, + htlc_payment_preimage=bfh(test_htlc['preimage']), + remote_htlc_sig=htlc_descs[output_index]['RemoteSigHex'], + success=test_htlc['incoming'], + cltv_timeout=test_htlc['expiry'] if not test_htlc['incoming'] else 0, # expiry is for timeout transaction + local_feerate_per_kw=local_feerate_per_kw, + our_commit_tx=our_commit_tx, + has_anchors=True + ) + ref_htlc = htlc_descs[output_index]['ResolutionTxHex'] + self.assertEqual(our_htlc, ref_htlc) # only works without r value grinding + finally: + lnutil.effective_htlc_tx_weight = effective_htlc_tx_weight_original + def sign_and_insert_remote_sig(self, tx: PartialTransaction, remote_pubkey, remote_signature, pubkey, privkey): assert type(remote_pubkey) is bytes assert len(remote_pubkey) == 33 diff --git a/electrum/util.py b/electrum/util.py index ebbbdb92ddb3..e5238f13d42d 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -139,6 +139,11 @@ def __str__(self): return _("Insufficient funds") +class UneconomicFee(Exception): + def __str__(self): + return _("The fee for the transaction is higher than the funds gained from it.") + + class NoDynamicFeeEstimates(Exception): def __str__(self): return _('Dynamic fee estimates not available') diff --git a/electrum/wallet.py b/electrum/wallet.py index 081be26b448e..466d96d65793 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -346,6 +346,9 @@ def can_have_deterministic_lightning(self) -> bool: return False return self.keystore.can_have_deterministic_lightning_xprv() + def can_sign_without_user_interaction_if_have_password(self) -> bool: + return False + def init_lightning(self, *, password) -> None: assert self.can_have_lightning() assert self.db.get('lightning_xprv') is None @@ -362,6 +365,7 @@ def init_lightning(self, *, password) -> None: if self.network: self.network.run_from_another_thread(self.stop()) self.lnworker = LNWallet(self, ln_xprv) + self.lnworker.maybe_enable_anchors_store_password(password) if self.network: self.start_network(self.network) @@ -2429,6 +2433,8 @@ def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True): if old_pw is None and self.has_password(): raise InvalidPassword() self.check_password(old_pw) + if self.lnworker: + self.lnworker.maybe_enable_anchors_store_password(new_pw) if self.storage: if encrypt_storage: enc_version = self.get_available_storage_encryption_version() @@ -3127,9 +3133,11 @@ def get_master_public_key(self): def derive_pubkeys(self, c, i): return [self.keystore.derive_pubkey(c, i).hex()] - - - + def can_sign_without_user_interaction_if_have_password(self) -> bool: + if (isinstance(self.keystore, keystore.Software_KeyStore) + and not self.keystore.is_watching_only()): + return True + return False class Standard_Wallet(Simple_Deterministic_Wallet):