From 7926219d4b233e8904ff3a963b58094e9c06a6a8 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 11:00:12 +0200 Subject: [PATCH 01/20] prepare a channel to have anchors * add anchor ln features * peer.use_anchors is added * channel.has_anchors is added --- electrum/lnchannel.py | 7 +++++++ electrum/lnpeer.py | 4 ++++ electrum/lnutil.py | 12 ++++++++++++ 3 files changed, 23 insertions(+) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index dd08135daa08..fd0dcb447eb7 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -399,6 +399,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.""" @@ -711,6 +715,9 @@ 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(): diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 1435389dc861..9f15a3fa88c9 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_ANCHOR_OUTPUTS_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'") @@ -770,6 +773,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, []) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index c22e3815bbc4..00d1f7296fb3 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1017,6 +1017,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 From 6426e3a7661d9c0ae42460ce7ae7656094b8ee57 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Wed, 22 Sep 2021 11:12:38 +0200 Subject: [PATCH 02/20] add static payment key * in order to be able to sweep to_remote in an onchain backup scenario we need to retain the private key for the payment_basepoint * to facilitate the above, we open a channel derived from a static secret (tied to the wallet seed), the static_payment_key combined with the funding pubkey (multisig_key), which we can restore from the channel closing transaction --- electrum/lnchannel.py | 14 ++++++++++++-- electrum/lnpeer.py | 16 +++++++++++----- electrum/lnutil.py | 28 ++++++++++++++++++++++++++-- electrum/lnworker.py | 1 + electrum/tests/test_lnpeer.py | 3 +++ 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index fd0dcb447eb7..48b8640b410e 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -323,9 +323,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 @@ -442,8 +444,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, @@ -530,6 +537,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) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 9f15a3fa88c9..ab37ce709163 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -537,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) @@ -553,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, diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 00d1f7296fb3..3f93d09c086d 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -187,7 +187,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 +194,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,6 +520,16 @@ def derive_blinded_privkey(basepoint_secret: bytes, per_commitment_secret: bytes return int.to_bytes(sum, length=32, byteorder='big', signed=False) +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): assert type(amount_msat) is int assert type(local_feerate) is int diff --git a/electrum/lnworker.py b/electrum/lnworker.py index b8ababb27145..c88e405c0503 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -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] diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 772a3af72220..9ee8138c41e6 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -113,6 +113,9 @@ def is_lightning_backup(self): def is_mine(self, addr): return True + def get_new_sweep_address_for_channel(self): + return None + class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): MPP_EXPIRY = 2 # HTLC timestamps are cast to int, so this cannot be 1 From a3cd1fd63cf5e0a0cc92b81f92d6932dda581fc7 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 11:42:55 +0200 Subject: [PATCH 03/20] lnutil: update ctx fee calculation for anchors --- electrum/lnchannel.py | 4 ++++ electrum/lnpeer.py | 2 ++ electrum/lnutil.py | 14 +++++++++++--- electrum/tests/test_lnutil.py | 8 ++++---- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 48b8640b410e..813e999ef8e7 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -1180,6 +1180,7 @@ 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 @@ -1190,6 +1191,7 @@ def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int: feerate=2 * feerate, is_local_initiator=self.constraints.is_initiator, round_to_sat=False, + has_anchors=self.has_anchors() )[sender] max_send_msat = sender_balance_msat - sender_reserve_msat - fee_spike_buffer else: @@ -1377,6 +1379,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: @@ -1426,6 +1429,7 @@ def make_commitment(self, subject: HTLCOwner, this_point: bytes, ctn: int) -> Pa 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(): diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index ab37ce709163..d9099007502d 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -689,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 @@ -831,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/lnutil.py b/electrum/lnutil.py index 3f93d09c086d..688da3169572 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -37,6 +37,7 @@ HTLC_TIMEOUT_WEIGHT = 663 HTLC_SUCCESS_WEIGHT = 703 COMMITMENT_TX_WEIGHT = 724 +COMMITMENT_TX_WEIGHT_ANCHORS = 1124 HTLC_OUTPUT_WEIGHT = 172 LN_MAX_FUNDING_SAT = pow(2, 24) - 1 @@ -133,6 +134,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 +160,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") @@ -839,13 +843,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 diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 116cbe8cd6c8..89564c4f2853 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -527,7 +527,7 @@ 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), + 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) 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) @@ -614,7 +614,7 @@ 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), + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), htlcs=[]) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) @@ -643,7 +643,7 @@ 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), + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), htlcs=[]) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) @@ -710,7 +710,7 @@ 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), + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), htlcs=[]) 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' From 85c6a83b0bc2deb5c31b1955f525abdef3beb073 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 13:51:51 +0200 Subject: [PATCH 04/20] lnchannel+lnutil: change htlc output, send new sig * changes the htlc outputs' witness script to have a csv lock of 1 * send signatures for remote ctx with ANYONECANPAY|SINGLE * refactor htlc weight (useful for zero-fee-htlc) --- electrum/lnchannel.py | 23 ++++-- electrum/lnutil.py | 147 ++++++++++++++++++++++++---------- electrum/tests/test_lnutil.py | 46 +++++++---- 3 files changed, 152 insertions(+), 64 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 813e999ef8e7..66493025ae39 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 @@ -968,6 +968,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)) @@ -1029,6 +1033,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): @@ -1038,7 +1045,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): @@ -1184,7 +1192,7 @@ def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int: ) 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 + 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 if sender == initiator == LOCAL: # see https://github.com/lightningnetwork/lightning-rfc/pull/740 fee_spike_buffer = calc_fees_for_commitment_tx( num_htlcs=num_htlcs_in_ctx + int(not is_htlc_dust) + 1, @@ -1222,7 +1230,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). """ @@ -1234,9 +1242,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)) @@ -1422,7 +1430,8 @@ 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( diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 688da3169572..69610df48e6e 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -35,7 +35,9 @@ # 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 @@ -534,7 +536,15 @@ def derive_payment_basepoint(static_payment_secret: bytes, funding_pubkey: bytes ) -def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_delayedpubkey, success, to_self_delay): +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( @@ -544,7 +554,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 @@ -580,13 +590,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), @@ -612,17 +627,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), @@ -651,23 +675,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], @@ -687,12 +727,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 @@ -744,22 +787,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) @@ -820,19 +870,30 @@ def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], lo return htlc_outputs, c_outputs_filtered -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 HTLC_SUCCESS_WEIGHT_ANCHORS if success else 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 diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 89564c4f2853..349ccd9280ea 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -9,9 +9,10 @@ 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 @@ -481,23 +482,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" @@ -555,22 +556,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)) - - 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): + our_commit_tx, + False, + )) + + 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 +593,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) From 781611072aae597448f9f15701ca5772126a12c4 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 13:41:01 +0200 Subject: [PATCH 05/20] lnutil+lnchannel: add anchors, adapt to_remote * to_remote has now an additional csv lock of 1 * anchor outputs are added if to_local/remote outputs are present * funder balance is reduced to accomodate anchors --- electrum/lnchannel.py | 64 ++++++++++++------ electrum/lnutil.py | 118 ++++++++++++++++++++++++++++------ electrum/tests/test_lnutil.py | 16 +++-- 3 files changed, 154 insertions(+), 44 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 66493025ae39..0f884cf2c7b0 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -52,7 +52,7 @@ 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) + received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT) from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo from .lnhtlc import HTLCManager @@ -732,7 +732,7 @@ 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 @@ -1167,7 +1167,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 @@ -1193,25 +1193,42 @@ def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int: 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, has_anchors=self.has_anchors()) * 1000 - if sender == initiator == LOCAL: # see https://github.com/lightningnetwork/lightning-rfc/pull/740 + + # 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, - has_anchors=self.has_anchors() - )[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 @@ -1464,22 +1481,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, diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 69610df48e6e..fd2ef7d6a5fa 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -41,6 +41,7 @@ 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 @@ -848,26 +849,64 @@ 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 effective_htlc_tx_weight(success: bool, has_anchors: bool): @@ -942,7 +981,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) @@ -955,7 +995,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 @@ -972,7 +1017,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) @@ -1004,8 +1053,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)}) diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 349ccd9280ea..3e21238e33eb 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -529,7 +529,9 @@ def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self): 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, has_anchors=False), - htlcs=htlcs) + 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) @@ -633,7 +635,9 @@ def test_commitment_tx_with_one_output(self): 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, 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) @@ -662,7 +666,9 @@ def test_commitment_tx_with_fee_greater_than_funder_amount(self): 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, 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) @@ -729,7 +735,9 @@ def test_simple_commitment_tx_with_no_HTLCs(self): 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, 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) ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220' self.assertEqual(str(our_commit_tx), ref_commit_tx_str) From 3500246121b227d8f3c732fc4c080ece93cfef0f Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:14:46 +0200 Subject: [PATCH 06/20] tests: add anchor commitment test vectors from rfc --- electrum/tests/anchor-vectors.json | 241 +++++++++++++++++++++++++++++ electrum/tests/test_lnutil.py | 131 ++++++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 electrum/tests/anchor-vectors.json 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/test_lnutil.py b/electrum/tests/test_lnutil.py index 3e21238e33eb..3eb4e6416400 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 @@ -16,8 +18,11 @@ 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 @@ -39,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 = [ @@ -742,6 +787,92 @@ def test_simple_commitment_tx_with_no_HTLCs(self): 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): + 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 + 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 From 81d75565aac72aa063a6d0575eb0d851e9471229 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:31:01 +0200 Subject: [PATCH 07/20] lnsweep: update sweeps to_remote and htlcs * sweep to_remote output, as this is now a p2wsh (previously internal wallet address) * sweep htlc outputs with new scripts --- electrum/lnsweep.py | 128 ++++++++++++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 40 deletions(-) diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 7d24d3de0387..cbf9346a9b09 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -14,7 +14,7 @@ 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) + map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script) from .transaction import (Transaction, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint) from .simple_config import SimpleConfig @@ -142,7 +142,7 @@ def create_sweeptx_for_their_revoked_htlc( htlc_tx: Transaction, sweep_address: str) -> Optional[SweepInfo]: - x = analyze_ctx(chan, ctx) + x = extract_ctx_secrets(chan, ctx) if not x: return ctn, their_pcp, is_revocation, per_commitment_secret = x @@ -182,10 +182,18 @@ def create_sweeptxs_for_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 +214,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) \ @@ -297,7 +305,7 @@ def create_txns_for_htlc( return txs -def analyze_ctx(chan: 'Channel', ctx: Transaction): +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) @@ -330,26 +338,33 @@ def create_sweeptxs_for_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) \ @@ -371,26 +386,35 @@ def create_sweeptxs_for_their_ctx( their_htlc_pubkey = derive_pubkey(their_conf.htlc_basepoint.pubkey, their_pcp) # to_local is handled by lnwatcher # to_remote + csv_delay = 0 if not chan.is_static_remotekey_enabled(): 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) - 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( - sweep_address=sweep_address, - ctx=ctx, - output_idx=output_idx, - our_payment_privkey=our_payment_privkey, - config=chan.lnworker.config) - txs[prevout] = SweepInfo( - name='their_ctx_to_remote', - csv_delay=0, - cltv_expiry=0, - gen_tx=sweep_tx) + else: + our_payment_privkey = ecc.ECPrivkey(our_conf.payment_basepoint.privkey) + if chan.has_anchors(): + csv_delay = 1 + + 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( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + our_payment_privkey=our_payment_privkey, + config=chan.lnworker.config, + has_anchors=chan.has_anchors() + ) + txs[prevout] = SweepInfo( + name='their_ctx_to_remote', + csv_delay=csv_delay, + cltv_expiry=0, + gen_tx=sweep_tx) + # HTLCs def create_sweeptx_for_htlc( htlc: 'UpdateAddHtlc', is_received_htlc: bool, @@ -405,9 +429,12 @@ 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( ctx=ctx, @@ -418,10 +445,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" @@ -471,7 +500,9 @@ def create_sweeptx_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]: 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 +511,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) @@ -501,24 +534,39 @@ def create_sweeptx_their_ctx_htlc( def create_sweeptx_their_ctx_to_remote( sweep_address: str, ctx: Transaction, output_idx: int, our_payment_privkey: ecc.ECPrivkey, - config: SimpleConfig) -> Optional[PartialTransaction]: + 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.script_type = 'p2wpkh' 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] - 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 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 From 8a7ea749e58ef7b86dca9ff0414f32411d4ca4a7 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:46:29 +0200 Subject: [PATCH 08/20] lnwatcher: renaming and comments for clarity --- electrum/lnwatcher.py | 47 +++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index fa2aba339079..87868b012081 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,44 +374,55 @@ 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 + # detect who closed and get information about how to claim outputs sweep_info_dict = chan.sweep_ctx(closing_tx) 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(): name = sweep_info.name + ' ' + chan.get_id_for_log() - spender_txid = spenders.get(prevout) + spender_txid = spenders.get(swept_output) if spender_txid is not None: + # TODO: spender should be the htlc transaction, but could also be a to_local/to_remote sweep 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: + htlc_revocation_sweep_info = chan.maybe_sweep_revoked_htlc(closing_tx, spender_tx) + if htlc_revocation_sweep_info: + # check if we already redeemed revoked htlc spender2 = spenders.get(spender_txid+':0') if spender2: keep_watching |= not self.is_deeply_mined(spender2) else: - await self.try_redeem(spender_txid+':0', e_htlc_tx, chan_id_for_log, name) + await self.try_redeem(spender_txid+':0', htlc_revocation_sweep_info, chan_id_for_log, name) keep_watching = True else: + # regular htlc transaction spending the htlc output of ctx, + # check if we can extract preimage from the input keep_watching |= not self.is_deeply_mined(spender_txid) - txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout)) + txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(swept_output)) assert txin_idx is not None spender_txin = spender_tx.inputs()[txin_idx] chan.extract_preimage_from_htlc_txin(spender_txin) - else: - 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() From 63d0a6ad9409009b522f9fb6182db9939e519ae7 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:47:41 +0200 Subject: [PATCH 09/20] lnwatcher: fix early cltv-locked claiming --- electrum/lnwatcher.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index 87868b012081..6b64d3d3a498 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -426,15 +426,17 @@ async def try_redeem(self, prevout: str, sweep_info: 'SweepInfo', chan_id_for_lo 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) tx = sweep_info.gen_tx() From f456024c8e8d0a3ffa743fefbf92eec2ec00ecc2 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:48:48 +0200 Subject: [PATCH 10/20] lnwatcher: add field for onchain htlc settlement control --- electrum/lnwatcher.py | 5 +++++ electrum/lnworker.py | 1 + 2 files changed, 6 insertions(+) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index 6b64d3d3a498..b81b96d73435 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -439,6 +439,11 @@ async def try_redeem(self, prevout: str, sweep_info: 'SweepInfo', chan_id_for_lo 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 c88e405c0503..2e1f3dbeb5e6 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -621,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! From b88585b3aaf222a3a5e169bc3b979a6458289580 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Wed, 15 Sep 2021 09:47:58 +0200 Subject: [PATCH 11/20] backups: restore from closing tx, sweep to_remote * add a method for backups to sweep to_remote * to_remote sweeping needs the payment_basepoint's private key to sign the sweep transaction * we restore the private key from our funding multisig pubkey (pubished with the closing transaction) and a static payment key secret * lower the final balance of the backup regtest, which is due to additional sweep transactions --- electrum/lnchannel.py | 15 +++- electrum/lnsweep.py | 136 ++++++++++++++++++++++-------- electrum/lnutil.py | 13 ++- electrum/tests/regtest/regtest.sh | 3 +- 4 files changed, 128 insertions(+), 39 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 0f884cf2c7b0..c01a4d66bc7a 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -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, FIXED_ANCHOR_SAT) + received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT, + ctx_has_anchors) from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo +from .lnsweep import create_sweeptx_their_backup_ctx from .lnhtlc import HTLCManager from .lnmsg import encode_msg, decode_msg from .address_synchronizer import TX_HEIGHT_LOCAL @@ -492,14 +494,19 @@ def is_backup(self): return True def create_sweeptxs_for_their_ctx(self, ctx): - return {} + return create_sweeptx_their_backup_ctx(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) else: - # backup from op_return - return {} + return + + def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]: + return None + + def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None: + return None def get_funding_address(self): return self.cb.funding_address diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index cbf9346a9b09..991afed6011e 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -14,14 +14,15 @@ 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, make_commitment_output_to_remote_witness_script) -from .transaction import (Transaction, TxOutput, PartialTransaction, PartialTxInput, - PartialTxOutput, TxOutpoint) + map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script, + derive_payment_basepoint, ctx_has_anchors, SCRIPT_TEMPLATE_FUNDING) +from .transaction import (Transaction, TxInput, PartialTransaction, PartialTxInput, + PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template) from .simple_config import SimpleConfig from .logging import get_logger, Logger if TYPE_CHECKING: - from .lnchannel import Channel, AbstractChannel + from .lnchannel import Channel, AbstractChannel, ChannelBackup _logger = get_logger(__name__) @@ -334,6 +335,69 @@ def extract_ctx_secrets(chan: 'Channel', ctx: Transaction): return ctn, their_pcp, is_revocation, per_commitment_secret +def extract_funding_pubkeys_from_ctx(txin: TxInput) -> Tuple[bytes, bytes]: + """Extract the two funding pubkeys from the published commitment transaction. + + We expect to see a witness script of: OP_2 pk1 pk2 OP_2 OP_CHECKMULTISIG""" + elements = txin.witness_elements() + witness_script = elements[-1] + assert match_script_against_template(witness_script, SCRIPT_TEMPLATE_FUNDING) + parsed_script = [x for x in script_GetOp(witness_script)] + pubkey1 = parsed_script[1][1] + pubkey2 = parsed_script[2][1] + return (pubkey1, pubkey2) + + +def create_sweeptx_their_backup_ctx( + *, chan: 'ChannelBackup', + ctx: Transaction, + sweep_address: str) -> Optional[Dict[str, SweepInfo]]: + txs = {} # type: Dict[str, SweepInfo] + """If we only have a backup, and the remote force-closed with their ctx, + and anchors are enabled, we need to sweep to_remote.""" + + if ctx_has_anchors(ctx): + # for anchors we need to sweep to_remote + funding_pubkeys = extract_funding_pubkeys_from_ctx(ctx.inputs()[0]) + _logger.debug(f'checking their ctx for funding pubkeys: {[pk.hex() for pk in funding_pubkeys]}') + # check which of the pubkey was ours + for pubkey in funding_pubkeys: + candidate_basepoint = derive_payment_basepoint(chan.lnworker.static_payment_key.privkey, funding_pubkey=pubkey) + candidate_to_remote_address = make_commitment_output_to_remote_address(candidate_basepoint.pubkey, has_anchors=True) + if ctx.get_output_idxs_from_address(candidate_to_remote_address): + our_payment_pubkey = candidate_basepoint + to_remote_address = candidate_to_remote_address + _logger.debug(f'found funding pubkey') + break + else: + return + else: + # we are dealing with static_remotekey which is locked to a wallet address + return {} + + # to_remote + csv_delay = 1 + our_payment_privkey = ecc.ECPrivkey(our_payment_pubkey.privkey) + output_idxs = ctx.get_output_idxs_from_address(to_remote_address) + if output_idxs: + output_idx = output_idxs.pop() + prevout = ctx.txid() + ':%d' % output_idx + sweep_tx = lambda: create_sweeptx_their_ctx_to_remote( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + our_payment_privkey=our_payment_privkey, + config=chan.lnworker.config, + has_anchors=True + ) + txs[prevout] = SweepInfo( + name='their_ctx_to_remote_backup', + csv_delay=csv_delay, + cltv_expiry=0, + gen_tx=sweep_tx) + return txs + + def create_sweeptxs_for_their_ctx( *, chan: 'Channel', ctx: Transaction, @@ -370,6 +434,8 @@ def create_sweeptxs_for_their_ctx( 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) @@ -380,42 +446,46 @@ 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 csv_delay = 0 - if not chan.is_static_remotekey_enabled(): + 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) - else: - our_payment_privkey = ecc.ECPrivkey(our_conf.payment_basepoint.privkey) - if chan.has_anchors(): - csv_delay = 1 - - 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( - sweep_address=sweep_address, - ctx=ctx, - output_idx=output_idx, - our_payment_privkey=our_payment_privkey, - config=chan.lnworker.config, - has_anchors=chan.has_anchors() - ) - txs[prevout] = SweepInfo( - name='their_ctx_to_remote', - csv_delay=csv_delay, - cltv_expiry=0, - gen_tx=sweep_tx) + 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( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + our_payment_privkey=our_payment_privkey, + config=chan.lnworker.config, + has_anchors=chan.has_anchors() + ) + txs[prevout] = SweepInfo( + name='their_ctx_to_remote', + csv_delay=csv_delay, + cltv_expiry=0, + gen_tx=sweep_tx) # HTLCs + our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=their_pcp) + our_htlc_privkey = ecc.ECPrivkey.from_secret_scalar(our_htlc_privkey) + their_htlc_pubkey = derive_pubkey(their_conf.htlc_basepoint.pubkey, their_pcp) def create_sweeptx_for_htlc( htlc: 'UpdateAddHtlc', is_received_htlc: bool, ctx_output_idx: int) -> None: diff --git a/electrum/lnutil.py b/electrum/lnutil.py index fd2ef7d6a5fa..e665efb4025a 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, @@ -46,6 +46,8 @@ 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', '') @@ -1120,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()) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index b3b366f5272d..4696377a610e 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -154,7 +154,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 From 1d89e4e59932803273b314705c974b024eb8e94d Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 11 Oct 2021 11:18:33 +0200 Subject: [PATCH 12/20] qt: add anchor channel icon source: https://tabler-icons.io/anchor --- electrum/gui/icons/anchor.png | Bin 0 -> 3781 bytes electrum/gui/qt/channels_list.py | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 electrum/gui/icons/anchor.png diff --git a/electrum/gui/icons/anchor.png b/electrum/gui/icons/anchor.png new file mode 100644 index 0000000000000000000000000000000000000000..20b152fe813be1be0251e28e02c18033a6e54ff3 GIT binary patch literal 3781 zcmbtXXHXN`5{3kkNRwa?2?QxIq5@)}2nI!J2p}NR6{L4DC{ap?pa>Fr)zGA<5Fj+^ z5RoQQ6eEUS!X;7@s$3%Eao@c6|INIe+4IeQyWh_4ob%_z+%z>1JR)&~i;GLp$WYhf z09pSyAMe45v|30$0B($hfeu&sfaC%fmk`cK_xdd?ab?0j&e2M$do94;4wZl9O~jk$ zs3bl;O~b^OJSAX)n&+{Y<7CKFu&&Hnr*87_;VHD|rH^^nRNi^&UN;FXv^q*SnXP{< zw(~=TL+tq*Uw+VF(!*InSFU;tXkhj^569Zf*3C>JOGC<9_C1&_+oL}C7ypA9+F>lD zBLv()_XW-$iL2rG>#+xuwx@t`+@dR-mdZVbE}I$5H8zoOOE)4h^(WwUM7p`JcZNpn zi%bdbfy2W%(V zb2d;DzCL#{>qQL#05#0b+w0=JGz|w(P(w(@fNIybA2t%JXNC~7evaI_pIZO4|SZD zkv}%)yg0Zz1Fbs*l=1exz7Mash9zJdqmQyCyIcBMnR?yW^r(}Oxy1--8(ae*p8+7l`fcMs2rLl%MtpIIHfqkq{*MwvxOg3z- zG0gPuzF!YDufnRqvMq5I%(UL60iEw>;m{>Ab6Rvxg?91j;0CKPGwt1vaq8ilF&N2S zhwfIXLzb^7h!L$(w5yXbx$Vkd^%HNq_Z zd*AHMhWaT%SHS~PYYAW6KNrH^GE(0s$-59+%rh`3^bi9RvCp}Tm7%fDYAzw0m1>% z2UTYN?RLZC7aQA)xezJMvekFQM=tdI(5JW?)$0<-?Vm0{>ov6p$}7sPG(Cw+-_{TsWE_yo(wCNO9?o zkz`&f#CMUI;~HU9X62Ql^44U*1ih69BOfU~-7&=Qyuz90^fX85YOo`z`>nqn;YQVL zEyRex-{iAtTUAhIwVNO*Mmj!{z?gTZcTA1*KNs!;(-cQ11o~EV`LL#l&3cnx4#jY_Mrwa|fs$iK`aSIc z4$lS!O8t}WoiF#cOX%r-w0yj2$t{lccyey2LYW-!;rnT}SfagArjHc+>he4fr?`hi zIyq@omfEuh6ig~tO-6b>0EN{ncb@3v)LMX4@c!9FHyhK2%c_ql&A$us&o}pY|CFJ% z;BaFjyXcIG1a&Ac?UqNBdUcIOxA?22b7>MqtMafLasw6(B8$!0lP}7fhfZD=^!1{k zB;G%_MnAEVCwzE%!Nb=i&qzY{n)r^wLIYM#f&|u}99f4I!ERVPCgt2tv6c-m7;KaU z+8d8D=%AYCX+1af`AR*<(L~oAPmcZBZy}B%n1Y?<&Kfa6G{nH?URtS}L(Z-azUE_{ z;m6c}KaQ8j(4cb`=i%i4n;=KZ?%OkfZvhD4TP}kW861K8(qz!iS(YY!y~uYFR~>XN zU#IdooFjT49kp9@Z{kj^1qa%mfj=HIg-xl1)_1g|d!5FISRRm&LZAADzy^$?W1P;f?oDzbE4+j!nX+@tOz1A=|MON)yo zAb9xQK4T50&XCuT+v0xFq`##NCsXzJ0t^R}_iq>3YpvQaX+iuGwgtPn-H;uekegkM z#s+%G^A=~ra9Sn$LYXfrjBB>se9sJYuGZ@o#}Z)dN98=PIZD&n<&IofGi1}e^RJTz zs2l+zPqY56{_D7(5pLy|B1B?E<#UQ1KqBL}QoPWjTT@tunZA%*% zfI!d1Phg=D1#(nn2>d>-WEH2<7H`8#1QI9W>%=oS;?9sL?GJPr`Z~rXJ4i27;UdqL zw##uB_RHXa@lR%^*3%lm}M5IEjp4V&G$o3emVPAszM2vdu#5(w-S} zpTzWXN;O7)+b`ILDuGT~Khn&P9jHe;J}WxDgj^SJ{!Em&P+s;AgN9Wd$fcK%$(!36 z4*~Yp`@Zz`Lga8*$FA(9#8EKz*+DuNL-^ve3E2s!ug9Mvbtn&gUZV*I+5Miu7n)8t z;aj@g7FF#UD_5Z*_@=lol7@>slZ3tBuNmSQh%NFe^fz~fM21Ep6(rz1ngw^n7eSX8 zcb8f<$r7SFHq*NF3q_${@M_SE_dH#1MU)<$w(NbBbR!EY*?Vp%l4j}`LfmaD|8}c) zzn}Zjy|Fv53x@MvN|sZcaFB556T~?-kzdl;!F_$qqX&Ve<87q}%G+-3c$?zD^8 z8$vWGctR+)UloIJV)xptSsRq*%(P#pGS0Ga_%Xs4Edd)hhvHO*#WJo3C%CMlRE?GX zv|mz12n{bJEgTsiyG}`=)%}Qex&7wY2Fv#PSNhgka@PVStQ&^()F4bNgt{7-)!q9>B^&~ zv#v&jLEVDc#4)Zx*tA&m12X_}>1O%df^6uvtX+bBZpf3f`AH<))m(s>0V!I|5w8j5 z-j57pFXyjjBhh;Rw&+(t>+x#vKq@8MG1_dZ;j%j%^&1vy+J0wxLS$*i2o#!U$;Y4- zFp0-1VOBdpBx3f?BDSoMXL3ri-KoeRx8dTwt0ikjp{Dx$JE)E)SaSfAS-P{8k)Tg9 zxIE7#OvmUvGT&DcLQRLo1MjrIt?-SpNmz7vK2@<8nYve-3|3dqB&WT#*0z<;>>(1<)h-xRzh}^u&gUz&Y-H|^vku9dnk}360*4r&FCyewah!U$!t)u z&n{S3`aS{VUb9IVDZT4k9A5{qRTrGPJ1Rvn|K{yd@EH_(Y=-!FJI1o$N3o1n?nAq% z;1_U{T3>H~au+j<={kO_OjYm!4^sM%8KH>}=mGb<2T(pqlp92n`LULyBbWQtSXUn_ zPOHuRPP>gTF`jOS!qCI^+hCTIClv<)|YzqvzOX@BFwlrDE2P*2K|=L z$yx*}bXsVZNLN|c>b43&gp1PNc4t%a1519?y%NQ$kPmd*a3>?8_vfI}7no#w${qzu zzg4PSdx|B}(bVKEa=Rim${#-Yd6Jtb`PT00k4y>^m~(RS??2`T8noCUY0w55Ngx30Owu=Acn&l>l4n*ON|VBesKsF+dvS`6ZLF3hBN3HUA1@rdo0Zy~gK! zUh$a(c!y_p@@g$#*qo@!8ARm9KC;HLSs*lJr$Pp0I(>+8xuea;J0GH2_1kYYI9ANr zHsiY-HNfIMDD)Kh$IK2p#z&TFCeV?oHm0x*b`NOz*cRIfXYnDM%(kK0tco`_FS!^X)($wF5X^1tpY$(gqS<8Vki+3eOXwL3iI6MJfWq(Ypih#LxNbOl|ZY zI}Q#YD0VKpsM+xvG}@qG7we8?(~&h?*_-F3>(e*0ttyqb!D7KbiS7$yULB57=HE16 zp1J%<`2IuX^5`)1MAB=qL2m1tUHX{hhEE_&#_!}-m!Gf@K6{4b;pF79kBK{M?3}^AL_~4qQs3R^maRpq?#g@B3z=q&69ZM zML&f8t>7#au;>~KX&QTo6!s%3rr8)`SDOedL7zqW>i7)M>r}^x;ezB|sUxA>JBopI z)S7r}5dSY6j3AH5|LwmZrDOATHSQ5nNHS~lMdKLg&tufMF)a@bHU}`g6lAh8fC#Wy z+RK*QmD}yJ%fj5RK_Is0oJ%4smE!jJxJ*Na$TmFuurmYyEQ&F(fHR#OZbjEy;c7qS tDuYiI_`;X%+8{J=k*e(fViwIDpp)x(nQX?(e?F>QMtY{Y 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: From 205d6cb346230d635db23efa79fd65c04f2aeed4 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:49:08 +0200 Subject: [PATCH 13/20] enable anchor outputs via config option * anchor channels can be activated via `enable_anchor_channels` config option * for anchor-enabled wallets, we store the wallet password, because UTXOs need to be added to commitment or HTLC transactions dynamically --- electrum/daemon.py | 2 ++ electrum/gui/kivy/main_window.py | 4 +++- electrum/gui/qt/__init__.py | 4 +++- electrum/gui/qt/installwizard.py | 11 ++++++----- electrum/lnutil.py | 1 + electrum/lnworker.py | 18 +++++++++++++++++- electrum/tests/regtest/regtest.sh | 1 + electrum/tests/test_lnpeer.py | 7 +++++++ electrum/wallet.py | 14 +++++++++++--- 9 files changed, 51 insertions(+), 11 deletions(-) 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/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/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/lnutil.py b/electrum/lnutil.py index e665efb4025a..2dac710ed119 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1314,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_ANCHOR_OUTPUTS_OPT | LnFeatures.OPTION_ANCHOR_OUTPUTS_REQ ) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 2e1f3dbeb5e6..3c07a915c145 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 @@ -651,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_ANCHOR_OUTPUTS_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')) @@ -672,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/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index 4696377a610e..10f7776f18d6 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -76,6 +76,7 @@ if [[ $1 == "init" ]]; then rm -rf /tmp/$2/ agent="./run_electrum --regtest -D /tmp/$2" $agent create --offline > /dev/null + $agent setconfig --offline enable_anchor_channels True $agent setconfig --offline log_to_file True $agent setconfig --offline use_gossip True $agent setconfig --offline server 127.0.0.1:51001:t diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 9ee8138c41e6..d9fafd891395 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -116,6 +116,12 @@ def is_mine(self, addr): 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 @@ -157,6 +163,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()}") 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): From 632681768ebb78ffe37a00b369a5783e1753fc56 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:06:35 +0200 Subject: [PATCH 14/20] unit tests: test anchors in lnpeer and lnchannel * testing of anchor channels is controlled via TEST_ANCHOR_CHANNELS * rewrite tests in test_lnchannel.py --- electrum/tests/test_lnchannel.py | 237 ++++++++++++++++++------------- electrum/tests/test_lnpeer.py | 14 +- 2 files changed, 152 insertions(+), 99 deletions(-) diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py index eb6230e4ee13..95e06e214849 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,6 +41,7 @@ from . import ElectrumTestCase +TEST_ANCHOR_CHANNELS = False one_bitcoin_in_msat = bitcoin.COIN * 1000 @@ -48,7 +49,7 @@ 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=TEST_ANCHOR_CHANNELS): assert local_amount > 0 assert remote_amount > 0 channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index) @@ -107,6 +108,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 +121,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=TEST_ANCHOR_CHANNELS): if random_seed is None: # needed for deterministic randomness random_seed = os.urandom(32) random_gen = PRNG(random_seed) @@ -151,7 +154,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 +163,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 +208,9 @@ 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=TEST_ANCHOR_CHANNELS) + expected_value = 9999056 if 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 +222,9 @@ def assertOutputExistsByValue(self, tx, amt_sat): else: self.assertFalse() + def assertNumberNonAnchorOutputs(self, number, tx): + self.assertEqual(number, len(tx.outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) + @classmethod def setUpClass(cls): super().setUpClass() @@ -228,15 +235,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=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 +265,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 +363,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 +391,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 +403,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 +455,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 +504,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) @@ -638,27 +665,27 @@ def test_AddHTLCNegativeBalance(self): class TestAvailableToSpend(ElectrumTestCase): def test_DesyncHTLCs(self): alice_channel, bob_channel = create_test_channels() - 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)) 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,17 +695,17 @@ 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) @@ -715,10 +742,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 +781,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 +805,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 @@ -814,38 +841,61 @@ def check_bals(self, amt1, amt2): class TestDust(ElectrumTestCase): def test_DustLimit(self): + """Test that addition of an HTLC below the dust limit changes the balances.""" alice_channel, bob_channel = create_test_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=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 TEST_ANCHOR_CHANNELS else 0)) + # htlc in bob's case goes to miner fees + self.assertEqual(2, len(bob_ctx.outputs()) - (2 if 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=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 TEST_ANCHOR_CHANNELS else 0)] - bobs_original_outputs[1 + (2 if TEST_ANCHOR_CHANNELS else 0)]) + self.assertEqual(2, len(alice_channel.get_next_commitment(LOCAL).outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(2, len(bob_channel.get_next_commitment(LOCAL).outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(htlc_amt, alice_channel.total_msat(SENT) // 1000) + def force_state_transition(chanA, chanB): chanB.receive_new_commitment(*chanA.sign_next_commitment()) @@ -854,12 +904,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 d9fafd891395..daab4b39dcab 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -39,10 +39,17 @@ from electrum.lnutil import LOCAL, REMOTE from electrum.invoices import PR_PAID, PR_UNPAID -from .test_lnchannel import create_test_channels +from .test_lnchannel import create_test_channels as create_test_channels_anchors from .test_bitcoin import needs_test_with_all_chacha20_implementations from . import TestCaseForTestnet +TEST_ANCHOR_CHANNELS = False + + +def create_test_channels(*args, **kwargs): + return create_test_channels_anchors(*args, **kwargs, anchor_outputs=TEST_ANCHOR_CHANNELS) + + def keypair(): priv = ECPrivkey.generate_random_key().get_secret_bytes() k1 = Keypair( @@ -147,6 +154,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': TEST_ANCHOR_CHANNELS} + self.maybe_enable_anchors_store_password(None) self.pending_payments = defaultdict(asyncio.Future) for chan in chans: chan.lnworker = self @@ -253,6 +263,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: From cfb4a103ecc3c5010a6d8faf297baf6f5e0e1252 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Wed, 15 Sep 2021 10:27:46 +0200 Subject: [PATCH 15/20] regtest: adapt to anchor channels * tests are kept variable via TEST_ANCHOR_CHANNELS * add funds to bob to be able to bump htlc transaction --- electrum/tests/regtest/regtest.sh | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index 10f7776f18d6..8eaef4ca6ea7 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -2,6 +2,7 @@ export HOME=~ set -eu +TEST_ANCHOR_CHANNELS=False # alice -> bob -> carol alice="./run_electrum --regtest -D /tmp/alice" @@ -76,7 +77,7 @@ if [[ $1 == "init" ]]; then rm -rf /tmp/$2/ agent="./run_electrum --regtest -D /tmp/$2" $agent create --offline > /dev/null - $agent setconfig --offline enable_anchor_channels True + $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 @@ -84,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 @@ -129,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 @@ -259,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 @@ -307,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 @@ -356,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 From 421f32d0557d9425f5c22a546c17416f3a5ef046 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Fri, 15 Oct 2021 11:08:10 +0200 Subject: [PATCH 16/20] anchors: switch to zero-fee-htlcs * sets the weight of htlc transactions to zero, thereby putting a zero fee for the htlc transactions * add inputs to htlc-tx for fee bumping * switches feature flags * disable anchor test vectors, which are now partially invalid --- electrum/lnpeer.py | 2 +- electrum/lnsweep.py | 102 ++++++++++++++++++++++++++++++---- electrum/lnutil.py | 4 +- electrum/lnworker.py | 2 +- electrum/tests/test_lnutil.py | 3 + electrum/util.py | 5 ++ 6 files changed, 104 insertions(+), 14 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index d9099007502d..1def2467fb8f 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -512,7 +512,7 @@ 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_ANCHOR_OUTPUTS_OPT) + 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']: diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 991afed6011e..b8a78f70d599 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, @@ -27,6 +28,9 @@ _logger = get_logger(__name__) +HTLC_TRANSACTION_DEADLINE_FRACTION = 4 +HTLC_TRANSACTION_SWEEP_TARGET = 10 + class SweepInfo(NamedTuple): name: str @@ -298,11 +302,14 @@ 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: + create_txns_for_htlc( + htlc=htlc, + htlc_direction=direction, + ctx_output_idx=ctx_output_idx, + htlc_relative_idx=htlc_relative_idx) + except UneconomicFee: + continue return txs @@ -548,7 +555,7 @@ def create_htlctx_that_spends_from_our_ctx( assert (htlc_direction == RECEIVED) == bool(preimage), 'preimage is required iff htlc is received' preimage = preimage or b'' ctn = extract_ctn_from_tx_and_chan(ctx, chan) - witness_script, htlc_tx = make_htlc_tx_with_open_channel( + witness_script, maybe_zero_fee_htlc_tx = make_htlc_tx_with_open_channel( chan=chan, pcp=our_pcp, subject=LOCAL, @@ -558,12 +565,87 @@ def create_htlctx_that_spends_from_our_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()[0].address + htlc_output_idx = funded_htlc_tx.get_output_idxs_from_address(htlc_out_address).pop() + inputs = funded_htlc_tx.inputs() + outputs = funded_htlc_tx.outputs() + if htlc_input_idx != 0: + htlc_txin = inputs.pop(htlc_input_idx) + inputs.insert(0, htlc_txin) + if htlc_output_idx != 0: + htlc_txout = outputs.pop(htlc_output_idx) + outputs.insert(0, 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: + 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(htlc_tx.sign_txin(0, local_htlc_privkey)) - txin = htlc_tx.inputs()[0] + local_htlc_sig = bfh(final_htlc_tx.sign_txin(0, local_htlc_privkey)) + txin = final_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 + return witness_script, final_htlc_tx def create_sweeptx_their_ctx_htlc( diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 2dac710ed119..768405ba27f4 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -917,7 +917,7 @@ def effective_htlc_tx_weight(success: bool, has_anchors: bool): # 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 HTLC_SUCCESS_WEIGHT_ANCHORS if success else HTLC_TIMEOUT_WEIGHT_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 @@ -1314,7 +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_ANCHOR_OUTPUTS_OPT | LnFeatures.OPTION_ANCHOR_OUTPUTS_REQ + | LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT | LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_REQ ) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 3c07a915c145..d83722971d0e 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -659,7 +659,7 @@ def maybe_enable_anchors_store_password(self, password): 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_ANCHOR_OUTPUTS_OPT + self.features |= LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT self.wallet_password = password else: if self.has_anchor_channels(): diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 3eb4e6416400..239a2cf69b7f 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -787,6 +787,9 @@ def test_simple_commitment_tx_with_no_HTLCs(self): ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220' self.assertEqual(str(our_commit_tx), ref_commit_tx_str) + @unittest.skip("only valid for original anchor ouputs, " + "but invalid due to different fee estimation " + "with anchors-zero-fee-htlcs") @disable_ecdsa_r_value_grinding def test_commitment_tx_anchors_test_vectors(self): for test_vector in ANCHOR_TEST_VECTORS: 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') From 3f9c530743d516ad50df58b96c4f26f39056f576 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 8 Nov 2021 15:26:41 +0100 Subject: [PATCH 17/20] tests: tests for both anchors and old ctx types * in test_lnutil, patch htlc weight to pass original anchor commitment test vectors * activate tests for both commitment types --- electrum/tests/__init__.py | 1 + electrum/tests/regtest.py | 22 +++- electrum/tests/regtest/regtest.sh | 2 +- electrum/tests/test_lnchannel.py | 53 +++++---- electrum/tests/test_lnpeer.py | 59 +++++----- electrum/tests/test_lnutil.py | 180 ++++++++++++++++-------------- 6 files changed, 178 insertions(+), 139 deletions(-) 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/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 8eaef4ca6ea7..58f62b1a6d75 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -2,7 +2,6 @@ export HOME=~ set -eu -TEST_ANCHOR_CHANNELS=False # alice -> bob -> carol alice="./run_electrum --regtest -D /tmp/alice" @@ -73,6 +72,7 @@ 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" diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py index 95e06e214849..afcb175c9074 100644 --- a/electrum/tests/test_lnchannel.py +++ b/electrum/tests/test_lnchannel.py @@ -41,15 +41,13 @@ from . import ElectrumTestCase -TEST_ANCHOR_CHANNELS = False - 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, anchor_outputs=TEST_ANCHOR_CHANNELS): + r_csv, anchor_outputs): assert local_amount > 0 assert remote_amount > 0 channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index) @@ -122,7 +120,7 @@ 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, - anchor_outputs=TEST_ANCHOR_CHANNELS): + anchor_outputs=False): if random_seed is None: # needed for deterministic randomness random_seed = os.urandom(32) random_gen = PRNG(random_seed) @@ -208,10 +206,11 @@ class TestFee(ElectrumTestCase): def test_fee(self): alice_channel, bob_channel = create_test_channels(feerate=253, local_msat=10000000000, - remote_msat=5000000000, anchor_outputs=TEST_ANCHOR_CHANNELS) - expected_value = 9999056 if TEST_ANCHOR_CHANNELS else 9999817 + 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 @@ -223,7 +222,7 @@ def assertOutputExistsByValue(self, tx, amt_sat): self.assertFalse() def assertNumberNonAnchorOutputs(self, number, tx): - self.assertEqual(number, len(tx.outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(number, len(tx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) @classmethod def setUpClass(cls): @@ -235,7 +234,7 @@ 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(anchor_outputs=TEST_ANCHOR_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) @@ -558,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) @@ -662,9 +660,13 @@ 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() + 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)) @@ -710,9 +712,13 @@ def test_DesyncHTLCs(self): 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 @@ -839,10 +845,15 @@ 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): """Test that addition of an HTLC below the dust limit changes the balances.""" - alice_channel, bob_channel = create_test_channels() + 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) @@ -852,7 +863,7 @@ def test_DustLimit(self): paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(paymentPreimage) fee_per_kw = alice_channel.get_next_feerate(LOCAL) - success_weight = effective_htlc_tx_weight(success=True, has_anchors=TEST_ANCHOR_CHANNELS) + 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 @@ -874,13 +885,13 @@ def test_DustLimit(self): 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 TEST_ANCHOR_CHANNELS else 0)) + 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 TEST_ANCHOR_CHANNELS else 0)) + 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=TEST_ANCHOR_CHANNELS)[LOCAL] // 1000 + 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) @@ -891,12 +902,16 @@ def test_DustLimit(self): # 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 TEST_ANCHOR_CHANNELS else 0)] - bobs_original_outputs[1 + (2 if TEST_ANCHOR_CHANNELS else 0)]) - self.assertEqual(2, len(alice_channel.get_next_commitment(LOCAL).outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) - self.assertEqual(2, len(bob_channel.get_next_commitment(LOCAL).outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) + 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()) rev = chanB.revoke_current_commitment() diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index daab4b39dcab..bd108464c952 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -39,15 +39,10 @@ from electrum.lnutil import LOCAL, REMOTE from electrum.invoices import PR_PAID, PR_UNPAID -from .test_lnchannel import create_test_channels as create_test_channels_anchors +from .test_lnchannel import create_test_channels from .test_bitcoin import needs_test_with_all_chacha20_implementations -from . import TestCaseForTestnet - -TEST_ANCHOR_CHANNELS = False - -def create_test_channels(*args, **kwargs): - return create_test_channels_anchors(*args, **kwargs, anchor_outputs=TEST_ANCHOR_CHANNELS) +from . import TestCaseForTestnet def keypair(): @@ -135,7 +130,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): 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) @@ -155,7 +150,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que 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': TEST_ANCHOR_CHANNELS} + self.config = {'enable_anchor_channels': has_anchors} self.maybe_enable_anchors_store_password(None) self.pending_payments = defaultdict(asyncio.Future) for chan in chans: @@ -379,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) @@ -405,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( @@ -412,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( @@ -419,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( @@ -426,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) @@ -547,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 @@ -567,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(): @@ -602,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)) @@ -631,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) @@ -695,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) @@ -1025,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) @@ -1059,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 @@ -1128,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)) @@ -1164,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(): @@ -1195,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(): @@ -1224,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(): @@ -1252,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 239a2cf69b7f..52560028070a 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -787,94 +787,102 @@ def test_simple_commitment_tx_with_no_HTLCs(self): ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220' self.assertEqual(str(our_commit_tx), ref_commit_tx_str) - @unittest.skip("only valid for original anchor ouputs, " - "but invalid due to different fee estimation " - "with anchors-zero-fee-htlcs") @disable_ecdsa_r_value_grinding def test_commitment_tx_anchors_test_vectors(self): - 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 + # 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 From f9210b418a2d5a2fcd1bad7236e6cd31ab0a4394 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Fri, 12 Nov 2021 10:34:50 +0100 Subject: [PATCH 18/20] sweep: rename sweep creation functions and reorder naming scheme: tx(s)_our/their_ctx/htlctx_output-description * functinon names are shortened to whether a single (tx) or several sweep transactions (txs) are generated * functions are ordered by their purpose (our/their ctx related) --- electrum/lnchannel.py | 20 +- electrum/lnsweep.py | 812 +++++++++++++++++++++--------------------- 2 files changed, 416 insertions(+), 416 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index c01a4d66bc7a..e8ab42f3e8fb 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -54,9 +54,9 @@ fee_for_htlc_output, offered_htlc_trim_threshold_sat, received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT, ctx_has_anchors) -from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx -from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo -from .lnsweep import create_sweeptx_their_backup_ctx +from .lnsweep import txs_our_ctx, txs_their_ctx +from .lnsweep import tx_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 @@ -223,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 @@ -494,11 +494,11 @@ def is_backup(self): return True def create_sweeptxs_for_their_ctx(self, ctx): - return create_sweeptx_their_backup_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address) + 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: return @@ -1316,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) @@ -1543,7 +1543,7 @@ def force_close_tx(self) -> PartialTransaction: 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) + return tx_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/lnsweep.py b/electrum/lnsweep.py index b8a78f70d599..4056f2307718 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -39,151 +39,49 @@ 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 - - -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 + 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_htlc( - chan: 'Channel', - ctx: Transaction, - htlc_tx: Transaction, - sweep_address: str) -> Optional[SweepInfo]: +def extract_funding_pubkeys_from_ctx(txin: TxInput) -> Tuple[bytes, bytes]: + """Extract the two funding pubkeys from the published commitment transaction. - x = extract_ctx_secrets(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) + 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_sweeptxs_for_our_ctx( +def txs_our_ctx( *, chan: 'AbstractChannel', ctx: Transaction, sweep_address: str) -> Optional[Dict[str, SweepInfo]]: @@ -234,7 +132,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, @@ -256,14 +154,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, @@ -273,7 +171,7 @@ 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( + sweep_tx = lambda: tx_sweep_our_htlctx( to_self_delay=to_self_delay, htlc_tx=htlc_tx, htlctx_witness_script=htlctx_witness_script, @@ -303,7 +201,7 @@ def create_txns_for_htlc( ctn=ctn) for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items(): try: - create_txns_for_htlc( + txs_htlc( htlc=htlc, htlc_direction=direction, ctx_output_idx=ctx_output_idx, @@ -313,99 +211,163 @@ def create_txns_for_htlc( return txs -def extract_ctx_secrets(chan: 'Channel', ctx: Transaction): - # note: the remote sometimes has two valid non-revoked commitment transactions, - # either of which could be broadcast - our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True) - ctn = extract_ctn_from_tx_and_chan(ctx, chan) - per_commitment_secret = None - oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE) - if ctn == oldest_unrevoked_remote_ctn: - their_pcp = their_conf.current_per_commitment_point - is_revocation = False - elif ctn == oldest_unrevoked_remote_ctn + 1: - their_pcp = their_conf.next_per_commitment_point - is_revocation = False - elif ctn < oldest_unrevoked_remote_ctn: # breach - try: - per_commitment_secret = chan.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn) - except UnableToDeriveSecret: - return - their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) - is_revocation = True - #_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 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. -def extract_funding_pubkeys_from_ctx(txin: TxInput) -> Tuple[bytes, bytes]: - """Extract the two funding pubkeys from the published commitment transaction. + 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 - 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 tx_our_ctx_htlctx( + chan: 'Channel', our_pcp: bytes, + ctx: Transaction, htlc: 'UpdateAddHtlc', + local_htlc_privkey: bytes, preimage: Optional[bytes], + htlc_direction: Direction, htlc_relative_idx: int, + ctx_output_idx: int) -> Tuple[bytes, Transaction]: + assert (htlc_direction == RECEIVED) == bool(preimage), 'preimage is required iff htlc is received' + preimage = preimage or b'' + ctn = extract_ctn_from_tx_and_chan(ctx, chan) + witness_script, 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)}') -def create_sweeptx_their_backup_ctx( - *, chan: 'ChannelBackup', - ctx: Transaction, - sweep_address: str) -> Optional[Dict[str, SweepInfo]]: - txs = {} # type: Dict[str, SweepInfo] - """If we only have a backup, and the remote force-closed with their ctx, - and anchors are enabled, we need to sweep to_remote.""" + # 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) - 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 {} + 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 - # to_remote - csv_delay = 1 - our_payment_privkey = ecc.ECPrivkey(our_payment_pubkey.privkey) - output_idxs = ctx.get_output_idxs_from_address(to_remote_address) - if output_idxs: - output_idx = output_idxs.pop() - prevout = ctx.txid() + ':%d' % output_idx - sweep_tx = lambda: create_sweeptx_their_ctx_to_remote( - sweep_address=sweep_address, - ctx=ctx, - output_idx=output_idx, - our_payment_privkey=our_payment_privkey, - config=chan.lnworker.config, - has_anchors=True + 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()[0].address + htlc_output_idx = funded_htlc_tx.get_output_idxs_from_address(htlc_out_address).pop() + inputs = funded_htlc_tx.inputs() + outputs = funded_htlc_tx.outputs() + if htlc_input_idx != 0: + htlc_txin = inputs.pop(htlc_input_idx) + inputs.insert(0, htlc_txin) + if htlc_output_idx != 0: + htlc_txout = outputs.pop(htlc_output_idx) + outputs.insert(0, 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 ) - txs[prevout] = SweepInfo( - name='their_ctx_to_remote_backup', - csv_delay=csv_delay, - cltv_expiry=0, - gen_tx=sweep_tx) - return txs + + 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: + 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(0, local_htlc_privkey)) + txin = final_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, final_htlc_tx -def create_sweeptxs_for_their_ctx( +def tx_sweep_our_htlctx( + *, 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 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=0, + 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]]: @@ -445,7 +407,7 @@ def create_sweeptxs_for_their_ctx( # 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( @@ -475,7 +437,7 @@ def create_sweeptxs_for_their_ctx( if output_idxs: output_idx = output_idxs.pop() prevout = ctx.txid() + ':%d' % output_idx - sweep_tx = lambda: create_sweeptx_their_ctx_to_remote( + sweep_tx = lambda: tx_their_ctx_to_remote( sweep_address=sweep_address, ctx=ctx, output_idx=output_idx, @@ -493,7 +455,7 @@ def create_sweeptxs_for_their_ctx( 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 create_sweeptx_for_htlc( + def tx_htlc( htlc: 'UpdateAddHtlc', is_received_htlc: bool, ctx_output_idx: int) -> None: if not is_received_htlc and not is_revocation: @@ -513,7 +475,7 @@ def create_sweeptx_for_htlc( 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, @@ -539,151 +501,83 @@ 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, 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()[0].address - htlc_output_idx = funded_htlc_tx.get_output_idxs_from_address(htlc_out_address).pop() - inputs = funded_htlc_tx.inputs() - outputs = funded_htlc_tx.outputs() - if htlc_input_idx != 0: - htlc_txin = inputs.pop(htlc_input_idx) - inputs.insert(0, htlc_txin) - if htlc_output_idx != 0: - htlc_txout = outputs.pop(htlc_output_idx) - outputs.insert(0, 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: - 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(0, local_htlc_privkey)) - txin = final_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, final_htlc_tx - + 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 = 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) + if sweep_tx: + txs.append(sweep_tx) + # HTLCs + 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_our_htlctx( + 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) -def create_sweeptx_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, - has_anchors: bool -) -> Optional[PartialTransaction]: - 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 - prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) - txin = PartialTxInput(prevout=prevout) - txin._trusted_value_sats = val - txin.witness_script = witness_script - txin.script_sig = b'' - 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) - outvalue = val - fee - if outvalue <= dust_threshold(): return None - sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] - tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry) - sig = bfh(tx.sign_txin(0, privkey)) - if not is_revocation: - witness = construct_witness([sig, preimage, witness_script]) - else: - revocation_pubkey = privkey_to_pubkey(privkey) - witness = construct_witness([sig, revocation_pubkey, witness_script]) - tx.inputs()[0].witness = bfh(witness) - assert tx.is_complete() - return tx + 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 create_sweeptx_their_ctx_to_remote( +def tx_their_ctx_to_remote( sweep_address: str, ctx: Transaction, output_idx: int, our_payment_privkey: ecc.ECPrivkey, config: SimpleConfig, @@ -724,55 +618,161 @@ def create_sweeptx_their_ctx_to_remote( return sweep_tx -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 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.""" - privkey: either revocation_privkey or localdelayed_privkey - is_revocation: tells us which ^ - """ + 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 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, + has_anchors: bool +) -> Optional[PartialTransaction]: + 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 prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) txin = PartialTxInput(prevout=prevout) txin._trusted_value_sats = val - txin.script_sig = b'' txin.witness_script = witness_script + txin.script_sig = b'' + if has_anchors: + txin.nsequence = 1 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 + 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) outvalue = val - fee - if outvalue <= dust_threshold(): - return None + 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 + tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry) + sig = bfh(tx.sign_txin(0, privkey)) + if not is_revocation: + witness = construct_witness([sig, preimage, witness_script]) + else: + revocation_pubkey = privkey_to_pubkey(privkey) + witness = construct_witness([sig, revocation_pubkey, witness_script]) + tx.inputs()[0].witness = bfh(witness) + assert tx.is_complete() + return tx -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( +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 tx_their_htlctx_justice( + chan: 'Channel', + ctx: Transaction, + htlc_tx: Transaction, + sweep_address: str) -> Optional[SweepInfo]: + + x = extract_ctx_secrets(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: tx_sweep_our_htlctx( 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, - ) + 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) From f4ebe6e75f07049934438c537816d5fd94a6a941 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 15 Nov 2021 14:23:33 +0100 Subject: [PATCH 19/20] htlctx: deal with possible peer htlctx batching Due to anchor channel's sighash.SINGLE and sighash.ANYONECANPAY, several HTLC-transactions can be combined. This means we must watch for revoked outputs in the HTLC transaction not only at index 0 but at any index. Also, any input can now contain preimages which we have to extract. --- electrum/lnchannel.py | 11 +++-- electrum/lnsweep.py | 96 +++++++++++++++++++++++++++---------------- electrum/lnwatcher.py | 44 ++++++++++++-------- 3 files changed, 92 insertions(+), 59 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index e8ab42f3e8fb..abd72afcf0f0 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -55,7 +55,7 @@ received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT, ctx_has_anchors) from .lnsweep import txs_our_ctx, txs_their_ctx -from .lnsweep import tx_their_htlctx_justice, SweepInfo +from .lnsweep import txs_their_htlctx_justice, SweepInfo from .lnsweep import tx_their_ctx_to_remote_backup from .lnhtlc import HTLCManager from .lnmsg import encode_msg, decode_msg @@ -502,8 +502,8 @@ def create_sweeptxs_for_our_ctx(self, ctx): else: return - def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]: - return None + def maybe_sweep_revoked_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[int, SweepInfo]: + return {} def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None: return None @@ -1541,9 +1541,8 @@ def force_close_tx(self) -> PartialTransaction: assert tx.is_complete() return tx - def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]: - # look at the output address, check if it matches - return tx_their_htlctx_justice(self, ctx, htlc_tx, self.sweep_address) + def maybe_sweep_revoked_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[int, SweepInfo]: + return txs_their_htlctx_justice(self, ctx, htlc_tx, self.sweep_address) def has_pending_changes(self, subject: HTLCOwner) -> bool: next_htlcs = self.hm.get_htlcs_in_next_ctx(subject) diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 4056f2307718..d8c6750830af 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -30,6 +30,7 @@ HTLC_TRANSACTION_DEADLINE_FRACTION = 4 HTLC_TRANSACTION_SWEEP_TARGET = 10 +HTLCTX_INPUT_OUTPUT_INDEX = 0 class SweepInfo(NamedTuple): @@ -171,21 +172,25 @@ def txs_htlc( htlc_direction=htlc_direction, ctx_output_idx=ctx_output_idx, htlc_relative_idx=htlc_relative_idx) - sweep_tx = lambda: tx_sweep_our_htlctx( + # we sweep our ctx with HTLC transactions individually, therefore the CSV-locked output is always at + # index TIMELOCKED_HTLCTX_OUTPUT_INDEX + assert True + sweep_tx = lambda: tx_sweep_htlctx_output( to_self_delay=to_self_delay, htlc_tx=htlc_tx, + output_idx=HTLCTX_INPUT_OUTPUT_INDEX, htlctx_witness_script=htlctx_witness_script, sweep_address=sweep_address, privkey=our_localdelayed_privkey.get_secret_bytes(), is_revocation=False, config=chan.lnworker.config) # side effect - txs[htlc_tx.inputs()[0].prevout.to_str()] = SweepInfo( + txs[htlc_tx.inputs()[HTLCTX_INPUT_OUTPUT_INDEX].prevout.to_str()] = SweepInfo( name='first-stage-htlc', csv_delay=0, cltv_expiry=htlc_tx.locktime, gen_tx=lambda: htlc_tx) - txs[htlc_tx.txid() + ':0'] = SweepInfo( + txs[htlc_tx.txid() + f':{HTLCTX_INPUT_OUTPUT_INDEX}'] = SweepInfo( name='second-stage-htlc', csv_delay=to_self_delay, cltv_expiry=0, @@ -306,16 +311,16 @@ def fee_estimator(size): htlc_outpoint = TxOutpoint(txid=bfh(ctx.txid()), out_idx=ctx_output_idx) htlc_input_idx = funded_htlc_tx.get_input_idx_that_spent_prevout(htlc_outpoint) - htlc_out_address = maybe_zero_fee_htlc_tx.outputs()[0].address + htlc_out_address = maybe_zero_fee_htlc_tx.outputs()[HTLCTX_INPUT_OUTPUT_INDEX].address htlc_output_idx = funded_htlc_tx.get_output_idxs_from_address(htlc_out_address).pop() inputs = funded_htlc_tx.inputs() outputs = funded_htlc_tx.outputs() - if htlc_input_idx != 0: + if htlc_input_idx != HTLCTX_INPUT_OUTPUT_INDEX: htlc_txin = inputs.pop(htlc_input_idx) - inputs.insert(0, htlc_txin) - if htlc_output_idx != 0: + inputs.insert(HTLCTX_INPUT_OUTPUT_INDEX, htlc_txin) + if htlc_output_idx != HTLCTX_INPUT_OUTPUT_INDEX: htlc_txout = outputs.pop(htlc_output_idx) - outputs.insert(0, htlc_txout) + outputs.insert(HTLCTX_INPUT_OUTPUT_INDEX, htlc_txout) final_htlc_tx = PartialTransaction.from_io( inputs, outputs, @@ -339,15 +344,15 @@ def fee_estimator(size): # sign HTLC output remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc_relative_idx=htlc_relative_idx) - local_htlc_sig = bfh(final_htlc_tx.sign_txin(0, local_htlc_privkey)) - txin = final_htlc_tx.inputs()[0] + local_htlc_sig = bfh(final_htlc_tx.sign_txin(HTLCTX_INPUT_OUTPUT_INDEX, local_htlc_privkey)) + txin = final_htlc_tx.inputs()[HTLCTX_INPUT_OUTPUT_INDEX] witness_program = bfh(Transaction.get_preimage_script(txin)) txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program) return witness_script, final_htlc_tx -def tx_sweep_our_htlctx( - *, htlc_tx: Transaction, htlctx_witness_script: bytes, sweep_address: str, +def tx_sweep_htlctx_output( + *, htlc_tx: Transaction, output_idx: int, htlctx_witness_script: bytes, sweep_address: str, privkey: bytes, is_revocation: bool, to_self_delay: int = None, config: SimpleConfig) -> Optional[PartialTransaction]: """Create a txn that sweeps the output of a first stage htlc tx @@ -358,7 +363,7 @@ def tx_sweep_our_htlctx( return tx_ctx_to_local( sweep_address=sweep_address, ctx=htlc_tx, - output_idx=0, + output_idx=output_idx, witness_script=htlctx_witness_script, privkey=privkey, is_revocation=is_revocation, @@ -553,14 +558,14 @@ def txs_their_htlctx_justice(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction commit=ctx, htlc=htlc, ctx_output_idx=ctx_output_idx) - return tx_sweep_our_htlctx( + return tx_sweep_htlctx_output( htlc_tx=htlc_tx, + output_idx=HTLCTX_INPUT_OUTPUT_INDEX, htlctx_witness_script=htlc_tx_witness_script, sweep_address=sweep_address, privkey=other_revocation_privkey, is_revocation=True, config=chan.lnworker.config) - htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs( chan=chan, ctx=ctx, @@ -675,6 +680,7 @@ def tx_their_ctx_htlc( cltv_expiry: int, config: SimpleConfig, has_anchors: bool ) -> Optional[PartialTransaction]: + """Deals with normal (non-CSV timelocked) HTLC output sweeps.""" assert type(cltv_expiry) is int preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered) val = ctx.outputs()[output_idx].value @@ -736,19 +742,24 @@ def tx_their_ctx_justice( return None -def tx_their_htlctx_justice( +def txs_their_htlctx_justice( chan: 'Channel', ctx: Transaction, htlc_tx: Transaction, - sweep_address: str) -> Optional[SweepInfo]: + sweep_address: str) -> Dict[int, SweepInfo]: + """Creates justice transactions for every output in the HTLC transaction. + Due to anchor type channels it can happen that a remote party batches HTLC transactions, + which is why this method can return multiple SweepInfos. + """ x = extract_ctx_secrets(chan, ctx) if not x: - return + return {} ctn, their_pcp, is_revocation, per_commitment_secret = x if not is_revocation: - return - # prep + return {} + + # get HTLC constraints (secrets and locktime) pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) this_conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=False) other_revocation_privkey = derive_blinded_privkey( @@ -756,23 +767,36 @@ def tx_their_htlctx_justice( per_commitment_secret) to_self_delay = other_conf.to_self_delay this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp) - # same witness script as to_local + revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True) + # uses the same witness script as to_local witness_script = bh2u(make_commitment_output_to_local_witness_script( revocation_pubkey, to_self_delay, this_delayed_pubkey)) htlc_address = redeem_script_to_address('p2wsh', witness_script) - # check that htlc_tx is a htlc - if htlc_tx.outputs()[0].address != htlc_address: - return - gen_tx = lambda: tx_sweep_our_htlctx( - sweep_address=sweep_address, - htlc_tx=htlc_tx, - htlctx_witness_script=bfh(witness_script), - privkey=other_revocation_privkey, - is_revocation=True, - config=chan.lnworker.config) - return SweepInfo( - name='redeem_htlc2', - csv_delay=0, - cltv_expiry=0, - gen_tx=gen_tx) + + # check that htlc transaction contains at least an output that is supposed to be + # spent via a second stage htlc transaction + htlc_outputs_idxs = [idx for idx, output in enumerate(htlc_tx.outputs()) if output.address == htlc_address] + if not htlc_outputs_idxs: + return {} + + index_to_sweepinfo = {} + for output_idx in htlc_outputs_idxs: + # generate justice transactions + gen_tx = lambda: tx_sweep_htlctx_output( + sweep_address=sweep_address, + output_idx=output_idx, + htlc_tx=htlc_tx, + htlctx_witness_script=bfh(witness_script), + privkey=other_revocation_privkey, + is_revocation=True, + config=chan.lnworker.config + ) + index_to_sweepinfo[output_idx] = SweepInfo( + name='redeem_htlc2', + csv_delay=0, + cltv_expiry=0, + gen_tx=gen_tx + ) + + return index_to_sweepinfo diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index b81b96d73435..58c5e8d3db17 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -385,36 +385,46 @@ async def sweep_commitment_transaction(self, funding_outpoint, closing_tx, spend return False chan_id_for_log = chan.get_id_for_log() # detect who closed and get information about how to claim outputs - sweep_info_dict = chan.sweep_ctx(closing_tx) + sweep_info_dict = chan.sweep_ctx(closing_tx) # output -> SweepInfo + # spenders: output -> txid keep_watching = False if sweep_info_dict else not self.is_deeply_mined(closing_tx.txid()) # create and broadcast transactions - for swept_output, sweep_info in sweep_info_dict.items(): + for swept_output, sweep_info in sweep_info_dict.items(): # can be any sweep (l, r, htlc, second-htlc) name = sweep_info.name + ' ' + chan.get_id_for_log() + # the output is swept by a certain txid that we know of spender_txid = spenders.get(swept_output) if spender_txid is not None: - # TODO: spender should be the htlc transaction, but could also be a to_local/to_remote sweep + # was output already swept and published? spender_tx = self.db.get_transaction(spender_txid) if not spender_tx: keep_watching = True continue - htlc_revocation_sweep_info = chan.maybe_sweep_revoked_htlc(closing_tx, spender_tx) - if htlc_revocation_sweep_info: + + # TODO: type SweepInfos? + if not 'htlc' in name: + continue + # we check the scenario when the peer force closes and an HTLC transaction + # was published, whether the HTLC transaction includes revoked outputs + htlc_tx = spender_tx + htlc_txid = spender_txid + + # check if we can extract preimages from an HTLC transaction + # a peer could have combined several HTLC-output spending inputs + for txin in htlc_tx.inputs(): + chan.extract_preimage_from_htlc_txin(txin) + keep_watching |= not self.is_deeply_mined(htlc_txid) + + # check if the HTLC transaction contains revoked outputs and redeem + htlc_idx_to_sweepinfo = chan.maybe_sweep_revoked_htlcs(closing_tx, htlc_tx) + for idx, htlc_revocation_sweep_info in htlc_idx_to_sweepinfo.items(): # check if we already redeemed revoked htlc - spender2 = spenders.get(spender_txid+':0') - if spender2: - keep_watching |= not self.is_deeply_mined(spender2) + htlc_tx_spender = spenders.get(spender_txid + f':{idx}') + if htlc_tx_spender: + keep_watching |= not self.is_deeply_mined(htlc_tx_spender) else: - await self.try_redeem(spender_txid+':0', htlc_revocation_sweep_info, chan_id_for_log, name) + await self.try_redeem(spender_txid + f':{idx}', htlc_revocation_sweep_info, chan_id_for_log, name) keep_watching = True - else: - # regular htlc transaction spending the htlc output of ctx, - # check if we can extract preimage from the input - keep_watching |= not self.is_deeply_mined(spender_txid) - txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(swept_output)) - assert txin_idx is not None - spender_txin = spender_tx.inputs()[txin_idx] - chan.extract_preimage_from_htlc_txin(spender_txin) else: # we sweep either the to_local, to_remote, or HTLC transaction outputs await self.try_redeem(swept_output, sweep_info, chan_id_for_log, name) keep_watching = True From 5cb97c077c799aa6ebd06492645b9c4f738f85f8 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 15 Nov 2021 14:27:16 +0100 Subject: [PATCH 20/20] watchtower: only send first-stage HTLC justice txs Due to malleability of HTLC-transactions, we can't send presigned justice transactions for the second-stage HTLC transactions, which is why we now send first-stage justice transactions for anchor channels. --- electrum/lnsweep.py | 82 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index d8c6750830af..31daeab3b239 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -522,16 +522,19 @@ def txs_their_ctx_watchtower(chan: 'Channel', ctx: Transaction, per_commitment_s # prep 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) + 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 = [] - # to_local - revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True) + + # 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, this_delayed_pubkey)) + 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: @@ -541,14 +544,67 @@ def txs_their_ctx_watchtower(chan: 'Channel', ctx: Transaction, per_commitment_s ctx=ctx, output_idx=output_idx, witness_script=bfh(witness_script), - privkey=other_revocation_privkey, + privkey=watcher_revocation_privkey, is_revocation=True, config=chan.lnworker.config) if sweep_tx: txs.append(sweep_tx) - # HTLCs - def txs_their_htlctx_justice(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction, - ctx_output_idx: int) -> Optional[Transaction]: + + # 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, + 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, @@ -563,7 +619,7 @@ def txs_their_htlctx_justice(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction output_idx=HTLCTX_INPUT_OUTPUT_INDEX, htlctx_witness_script=htlc_tx_witness_script, sweep_address=sweep_address, - privkey=other_revocation_privkey, + privkey=watcher_revocation_privkey, is_revocation=True, config=chan.lnworker.config) htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(