From 55c0b1406324ae5dded43b2d9ec76d94d98a4690 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 11:00:12 +0200 Subject: [PATCH 01/19] prepare a channel to have anchors * add anchor ln features * peer.use_anchors is added * channel.has_anchors is added --- electrum/lnchannel.py | 8 ++++++++ electrum/lnpeer.py | 3 +++ electrum/lnutil.py | 17 +++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 18c5156cabcd..c16b630f1b94 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -479,6 +479,10 @@ def get_capacity(self) -> Optional[int]: def can_be_deleted(self) -> bool: pass + @abstractmethod + def has_anchors(self) -> bool: + pass + class ChannelBackup(AbstractChannel): """ @@ -856,6 +860,10 @@ def sweep_address(self) -> str: assert self.lnworker.wallet.is_mine(addr) return addr + def has_anchors(self) -> bool: + channel_type = ChannelType(self.storage.get('channel_type')) + return bool(channel_type & ChannelType.OPTION_ANCHOR_OUTPUTS) + def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]: assert self.is_static_remotekey_enabled() our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index f1639f75b08a..372b371db70b 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -653,6 +653,9 @@ def accepts_zeroconf(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'") diff --git a/electrum/lnutil.py b/electrum/lnutil.py index fbf34a2b87d8..19568f03bb97 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1190,10 +1190,27 @@ 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) + # Temporary number. OPTION_TRAMPOLINE_ROUTING_REQ_ECLAIR = 1 << 148 OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR = 1 << 149 + # This is still a temporary number. Also used by Eclair. + OPTION_TRAMPOLINE_ROUTING_REQ = 1 << 148 + OPTION_TRAMPOLINE_ROUTING_OPT = 1 << 149 + + _ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_REQ_ECLAIR] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE) _ln_feature_contexts[OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE) From c97189aa69469195c80ebe8c41f5943c69ebe801 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Wed, 22 Sep 2021 11:12:38 +0200 Subject: [PATCH 02/19] 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 | 9 ++++++++- electrum/lnpeer.py | 16 +++++++++++----- electrum/lnutil.py | 25 +++++++++++++++++++++++-- electrum/lnworker.py | 1 + 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index c16b630f1b94..bee311a60463 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -398,7 +398,6 @@ def update_closed_state(self, *, funding_txid: str, funding_height: TxMinedInfo, # auto-remove redeemed backups self.lnworker.remove_channel_backup(self.channel_id) - @abstractmethod def is_initiator(self) -> bool: pass @@ -522,8 +521,13 @@ def init_config(self, cb: ImportedChannelBackupStorage): 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 static_remotekey=local_payment_pubkey, # dummy values + static_payment_key=None, dust_limit_sat=None, max_htlc_value_in_flight_msat=None, max_accepted_htlcs=None, @@ -620,6 +624,9 @@ def is_frozen_for_receiving(self) -> bool: def sweep_address(self) -> str: return self.lnworker.wallet.get_new_sweep_address_for_channel() + 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 372b371db70b..44628371833b 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -678,11 +678,16 @@ 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'' - assert channel_type & channel_type.OPTION_STATIC_REMOTEKEY - wallet = self.lnworker.wallet - assert wallet.txin_type == 'p2wpkh' - addr = wallet.get_new_sweep_address_for_channel() - static_remotekey = bytes.fromhex(wallet.get_public_key(addr)) + if self.use_anchors(): + static_payment_key = self.lnworker.static_payment_key + static_remotekey = None + else: + assert channel_type & channel_type.OPTION_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 = bytes.fromhex(wallet.get_public_key(addr)) dust_limit_sat = bitcoin.DUST_LIMIT_P2PKH reserve_sat = max(funding_sat // 100, dust_limit_sat) @@ -693,6 +698,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.LIGHTNING_TO_SELF_DELAY_CSV, dust_limit_sat=dust_limit_sat, diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 19568f03bb97..b5aa4346aa67 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -212,7 +212,6 @@ class LocalConfig(ChannelConfig): @classmethod def from_seed(cls, **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 @@ -220,11 +219,23 @@ def from_seed(cls, **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) - if static_remotekey: + 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: # we expect all our channels to use option_static_remotekey, so ending up here likely indicates an issue... kwargs['payment_basepoint'] = keypair_generator(LnKeyFamily.PAYMENT_BASE) + return LocalConfig(**kwargs) def validate_params(self, *, funding_sat: int, config: 'SimpleConfig', peer_features: 'LnFeatures') -> None: @@ -591,6 +602,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, ) -> Tuple[bytes, PartialTxOutput]: diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 2b4e24df33d8..9b88153031a7 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -819,6 +819,7 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.db = wallet.db 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.payment_secret_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.PAYMENT_SECRET_KEY).privkey Logger.__init__(self) features = LNWALLET_FEATURES From 94a4fef1cf13a4cff410f31d7a1dbd0426ba193b Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 11:42:55 +0200 Subject: [PATCH 03/19] lnutil: update ctx fee calculation for anchors --- electrum/lnchannel.py | 4 ++++ electrum/lnpeer.py | 2 ++ electrum/lnutil.py | 14 +++++++++++--- tests/test_lnutil.py | 8 ++++---- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index bee311a60463..302eef5f804a 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -1328,6 +1328,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 @@ -1338,6 +1339,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: @@ -1520,6 +1522,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: @@ -1569,6 +1572,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(), ) assert self.is_static_remotekey_enabled() payment_pubkey = other_config.payment_basepoint.pubkey diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 44628371833b..e33427f45ec2 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -875,6 +875,7 @@ async def channel_establishment_flow( initial_feerate_per_kw=feerate, config=self.network.config, peer_features=self.features, + has_anchors=self.use_anchors(), ) # -> funding created @@ -1045,6 +1046,7 @@ async def on_open_channel(self, payload): initial_feerate_per_kw=feerate, config=self.network.config, peer_features=self.features, + has_anchors=self.use_anchors(), ) channel_flags = ord(payload['channel_flags']) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index b5aa4346aa67..7b69c83a8e7c 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -48,6 +48,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_LEGACY = pow(2, 24) - 1 @@ -158,6 +159,7 @@ def cross_validate_params( initial_feerate_per_kw: int, config: 'SimpleConfig', peer_features: 'LnFeatures', + has_anchors: bool, ) -> None: # first we validate the configs separately local_config.validate_params(funding_sat=funding_sat, config=config, peer_features=peer_features) @@ -183,7 +185,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") @@ -1009,13 +1013,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/tests/test_lnutil.py b/tests/test_lnutil.py index 1d05cbc5d24c..42acbbdea5ec 100644 --- a/tests/test_lnutil.py +++ b/tests/test_lnutil.py @@ -536,7 +536,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) @@ -623,7 +623,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) @@ -652,7 +652,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) @@ -719,7 +719,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 c162a12a29b0efebf03a5cde93b32c75de418f65 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 13:51:51 +0200 Subject: [PATCH 04/19] 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 | 21 +++++++--- electrum/lnutil.py | 90 +++++++++++++++++++++++++++++++------------ tests/test_lnutil.py | 42 ++++++++++++++------ 3 files changed, 110 insertions(+), 43 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 302eef5f804a..cf84b8bb9f0a 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -1114,6 +1114,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 = htlc_tx.sign_txin(0, their_remote_htlc_privkey) htlc_sig = ecc.ecdsa_sig64_from_der_sig(sig[:-1]) htlcsigs.append((ctx_output_idx, htlc_sig)) @@ -1182,6 +1186,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 = htlc_tx.serialize_preimage(0) msg_hash = sha256d(pre_hash) remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, pcp) @@ -1201,7 +1208,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.ecdsa_der_sig_from_ecdsa_sig64(htlc_sig) + Sighash.to_sigbytes(Sighash.ALL) + remote_sighash = Sighash.ALL if not self.has_anchors() else Sighash.ANYONECANPAY | Sighash.SINGLE + remote_htlc_sig = ecc.ecdsa_der_sig_from_ecdsa_sig64(htlc_sig) + remote_sighash.to_sigbytes(1, 'big') return remote_htlc_sig def revoke_current_commitment(self): @@ -1332,7 +1340,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, @@ -1370,7 +1378,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). """ @@ -1382,9 +1390,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)) @@ -1565,7 +1573,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_abs=htlc.cltv_abs), htlc)) + cltv_abs=htlc.cltv_abs, + 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 7b69c83a8e7c..17e6ee155810 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -46,7 +46,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 @@ -617,7 +619,13 @@ 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, + amount_msat, + local_feerate, + revocationpubkey, + local_delayedpubkey, + success, + to_self_delay, + has_anchors: bool ) -> Tuple[bytes, PartialTxOutput]: assert type(amount_msat) is int assert type(local_feerate) is int @@ -628,7 +636,7 @@ def make_htlc_tx_output( ) p2wsh = bitcoin.redeem_script_to_address('p2wsh', 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 @@ -670,12 +678,13 @@ def make_offered_htlc( 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 = construct_script([ + script_opcodes = [ opcodes.OP_DUP, opcodes.OP_HASH160, bitcoin.hash_160(revocation_pubkey), @@ -701,8 +710,11 @@ def make_offered_htlc( 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 = construct_script(script_opcodes) return script def make_received_htlc( @@ -712,12 +724,13 @@ def make_received_htlc( local_htlcpubkey: bytes, payment_hash: bytes, cltv_abs: int, + has_anchors: bool, ) -> bytes: for i in [revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash]: assert type(i) is bytes assert type(cltv_abs) is int - script = construct_script([ + script_opcodes = [ opcodes.OP_DUP, opcodes.OP_HASH160, bitcoin.hash_160(revocation_pubkey), @@ -746,8 +759,11 @@ def make_received_htlc( 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 = construct_script(script_opcodes) return script WITNESS_TEMPLATE_OFFERED_HTLC = [ @@ -820,18 +836,25 @@ def make_htlc_output_witness_script( local_htlc_pubkey: bytes, payment_hash: bytes, cltv_abs: 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_abs=cltv_abs) + return make_received_htlc( + revocation_pubkey=remote_revocation_pubkey, + remote_htlcpubkey=remote_htlc_pubkey, + local_htlcpubkey=local_htlc_pubkey, + payment_hash=payment_hash, + cltv_abs=cltv_abs, + 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], @@ -858,6 +881,7 @@ def possible_output_idxs_of_htlc_in_ctx(*, chan: 'Channel', pcp: bytes, subject: local_htlc_pubkey=htlc_pubkey, payment_hash=payment_hash, cltv_abs=cltv_abs, + has_anchors=chan.has_anchors(), ) htlc_address = redeem_script_to_address('p2wsh', witness_script) candidates = ctx.get_output_idxs_from_address(htlc_address) @@ -910,12 +934,14 @@ 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) + success=is_htlc_success, + to_self_delay=other_conf.to_self_delay, + has_anchors=chan.has_anchors(), + ) witness_script_in = make_htlc_output_witness_script( is_received_htlc=is_htlc_success, remote_revocation_pubkey=other_revocation_pubkey, @@ -923,11 +949,14 @@ def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTL local_htlc_pubkey=htlc_pubkey, payment_hash=payment_hash, cltv_abs=cltv_abs, + has_anchors=chan.has_anchors(), ) htlc_tx_inputs = make_htlc_tx_inputs( commit.txid(), ctx_output_idx, amount_msat=amount_msat, witness_script=witness_script_in) + if chan.has_anchors(): + htlc_tx_inputs[0].nsequence = 1 if is_htlc_success: cltv_abs = 0 htlc_tx = make_htlc_tx(cltv_abs=cltv_abs, inputs=htlc_tx_inputs, output=htlc_tx_output) @@ -990,19 +1019,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/tests/test_lnutil.py b/tests/test_lnutil.py index 42acbbdea5ec..0ef1a4230f48 100644 --- a/tests/test_lnutil.py +++ b/tests/test_lnutil.py @@ -11,6 +11,7 @@ get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, ln_compare_features, IncompatibleLightningFeatures, ChannelType, + offered_htlc_trim_threshold_sat, received_htlc_trim_threshold_sat, ImportedChannelBackupStorage) from electrum.util import bfh, MyEncoder from electrum.transaction import Transaction, PartialTransaction, Sighash @@ -490,23 +491,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(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[2])) + htlc[2] = make_offered_htlc(**htlc_pubkeys, payment_hash=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(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[3])) + htlc[3] = make_offered_htlc(**htlc_pubkeys, payment_hash=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(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[0]), cltv_abs=htlc_cltv_timeout[0]) + htlc[0] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[0]), cltv_abs=htlc_cltv_timeout[0], has_anchors=False) htlc_cltv_timeout[1] = 501 htlc_payment_preimage[1] = b"\x01" * 32 - htlc[1] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[1]), cltv_abs=htlc_cltv_timeout[1]) + htlc[1] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[1]), cltv_abs=htlc_cltv_timeout[1], has_anchors=False) htlc_cltv_timeout[4] = 504 htlc_payment_preimage[4] = b"\x04" * 32 - htlc[4] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[4]), cltv_abs=htlc_cltv_timeout[4]) + htlc[4] = make_received_htlc(**htlc_pubkeys, payment_hash=bitcoin.sha256(htlc_payment_preimage[4]), cltv_abs=htlc_cltv_timeout[4], has_anchors=False) remote_signature = bfh("304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606") output_commit_tx = "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220" @@ -564,22 +565,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_abs, 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_abs: 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, @@ -590,10 +602,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=local_sig, payment_preimage=htlc_payment_preimage if success else b'', # will put 00 on witness if timeout witness_script=htlc) From 557a4bcb3926b72e476a4cc9f9b2cb4f36e0779b Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 13:41:01 +0200 Subject: [PATCH 05/19] 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 | 66 +++++++++++++++-------- electrum/lnutil.py | 119 +++++++++++++++++++++++++++++++++++------- tests/test_lnutil.py | 16 ++++-- 3 files changed, 156 insertions(+), 45 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index cf84b8bb9f0a..87cdde899961 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -53,7 +53,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, ChannelType, LNProtocolWarning) from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo @@ -862,7 +862,7 @@ def sweep_address(self) -> str: addr = None assert 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=self.has_anchors()) if self.lnworker: assert self.lnworker.wallet.is_mine(addr) return addr @@ -874,7 +874,7 @@ def has_anchors(self) -> bool: def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]: assert 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()) return [to_remote_address] def get_feerate(self, subject: HTLCOwner, *, ctn: int) -> int: @@ -1315,7 +1315,7 @@ def has_unsettled_htlcs(self) -> bool: return len(self.hm.htlcs(LOCAL)) + len(self.hm.htlcs(REMOTE)) > 0 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 @@ -1341,25 +1341,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 @@ -1603,22 +1620,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=local_script, - remote_script=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=local_script, + remote_script=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 17e6ee155810..30cf219e2f1c 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -52,6 +52,7 @@ COMMITMENT_TX_WEIGHT = 724 COMMITMENT_TX_WEIGHT_ANCHORS = 1124 HTLC_OUTPUT_WEIGHT = 172 +FIXED_ANCHOR_SAT = 330 LN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1 DUST_LIMIT_MAX = 1000 @@ -997,26 +998,65 @@ class Direction(IntEnum): 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: bytes, remote_script: bytes, 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=local_script, value=to_local_amt // 1000) - to_remote_amt = max(0, remote_amount_msat - fees_per_participant[REMOTE]) - to_remote = PartialTxOutput(scriptpubkey=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: bytes, + remote_script: bytes, + 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', script) - htlc_outputs.append(PartialTxOutput(scriptpubkey=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)) +>>>>>>> 3d2216d18 (lnutil+lnchannel: add anchors, adapt to_remote) - # 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): @@ -1091,7 +1131,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) @@ -1104,7 +1145,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 @@ -1121,7 +1167,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) @@ -1153,8 +1203,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', local_script) -def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str: - return bitcoin.pubkey_to_address('p2wpkh', remote_payment_pubkey.hex()) +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', remote_script.hex()) + else: + return bitcoin.pubkey_to_address('p2wpkh', remote_payment_pubkey.hex()) + +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', script.hex()) def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config): tx.sign({local_config.multisig_key.pubkey: local_config.multisig_key.privkey}) diff --git a/tests/test_lnutil.py b/tests/test_lnutil.py index 0ef1a4230f48..6f4e57884ab4 100644 --- a/tests/test_lnutil.py +++ b/tests/test_lnutil.py @@ -538,7 +538,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) @@ -642,7 +644,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) @@ -671,7 +675,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) @@ -738,7 +744,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 774991d2340d80b24c5b0e1b1c28edf941a91d64 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:14:46 +0200 Subject: [PATCH 06/19] tests: add anchor commitment test vectors from rfc --- electrum/lnutil.py | 23 ++-- tests/anchor-vectors.json | 241 ++++++++++++++++++++++++++++++++++++++ tests/test_lnutil.py | 137 ++++++++++++++++++++++ 3 files changed, 389 insertions(+), 12 deletions(-) create mode 100644 tests/anchor-vectors.json diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 30cf219e2f1c..cfaa2fb70bc6 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1020,7 +1020,7 @@ def make_commitment_outputs( if htlc.amount_msat // 1000 > dust_limit_sat: htlc_outputs.append( PartialTxOutput( - scriptpubkey=bfh(address_to_script(addr)), + scriptpubkey=address_to_script(addr), value=htlc.amount_msat // 1000 )) @@ -1043,16 +1043,15 @@ def make_commitment_outputs( # 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)) + anchor_outputs.append(PartialTxOutput(scriptpubkey=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)) + anchor_outputs.append(PartialTxOutput(scriptpubkey=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)) ->>>>>>> 3d2216d18 (lnutil+lnchannel: add anchors, adapt to_remote) + non_htlc_outputs.append(PartialTxOutput(scriptpubkey=local_script, value=to_local_amt_msat // 1000)) + non_htlc_outputs.append(PartialTxOutput(scriptpubkey=remote_script, value=to_remote_amt_msat // 1000)) c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs)) c_outputs = c_outputs_filtered + anchor_outputs @@ -1205,24 +1204,24 @@ def make_commitment_output_to_local_address( def make_commitment_output_to_remote_witness_script(remote_payment_pubkey: bytes) -> bytes: assert isinstance(remote_payment_pubkey, bytes) - script = bfh(construct_script([ + script = 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', remote_script.hex()) + return bitcoin.redeem_script_to_address('p2wsh', remote_script) else: return bitcoin.pubkey_to_address('p2wpkh', remote_payment_pubkey.hex()) def make_commitment_output_to_anchor_witness_script(funding_pubkey: bytes) -> bytes: assert isinstance(funding_pubkey, bytes) - script = bfh(construct_script([ + script = construct_script([ funding_pubkey, opcodes.OP_CHECKSIG, opcodes.OP_IFDUP, @@ -1230,12 +1229,12 @@ def make_commitment_output_to_anchor_witness_script(funding_pubkey: bytes) -> by 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', script.hex()) + return bitcoin.redeem_script_to_address('p2wsh', script) def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config): tx.sign({local_config.multisig_key.pubkey: local_config.multisig_key.privkey}) diff --git a/tests/anchor-vectors.json b/tests/anchor-vectors.json new file mode 100644 index 000000000000..ac438fc0867c --- /dev/null +++ b/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/tests/test_lnutil.py b/tests/test_lnutil.py index 6f4e57884ab4..5b780ec73d77 100644 --- a/tests/test_lnutil.py +++ b/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 import ecc @@ -20,8 +22,11 @@ from electrum.simple_config import SimpleConfig from . import ElectrumTestCase, as_testnet +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 @@ -43,6 +48,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 = [ @@ -751,6 +796,98 @@ 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 = bfh(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( + revocation_pubkey=local_revocation_pubkey, + remote_htlcpubkey=remote_htlcpubkey, + local_htlcpubkey=local_htlcpubkey, + payment_hash=bitcoin.sha256(bfh(test_htlc['preimage'])), + cltv_abs=test_htlc['expiry'], + has_anchors=True) + else: + htlc_script = make_offered_htlc( + revocation_pubkey=local_revocation_pubkey, + remote_htlcpubkey=remote_htlcpubkey, + local_htlcpubkey=local_htlcpubkey, + payment_hash=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_abs=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_abs))} + 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_abs=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: bytes, remote_signature: bytes, pubkey: bytes, privkey: bytes): assert type(remote_pubkey) is bytes assert len(remote_pubkey) == 33 From 93e136df02aaef10a06326b9459300bcc8da91df Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:31:01 +0200 Subject: [PATCH 07/19] 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 | 111 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 90 insertions(+), 21 deletions(-) diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index e4744468b5b2..b247fee7a3af 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -16,7 +16,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 @@ -145,7 +145,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 @@ -185,10 +185,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) @@ -212,7 +220,7 @@ def create_sweeptxs_for_our_ctx( if not chan.is_backup(): assert chan.is_static_remotekey_enabled() their_payment_pubkey = their_conf.payment_basepoint.pubkey - to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey) + to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey, has_anchors=chan.has_anchors()) found_to_remote = bool(ctx.get_output_idxs_from_address(to_remote_address)) else: found_to_remote = False @@ -311,7 +319,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) @@ -344,17 +352,24 @@ 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 = make_commitment_output_to_local_witness_script( @@ -366,7 +381,7 @@ def create_sweeptxs_for_their_ctx( if not chan.is_backup(): assert chan.is_static_remotekey_enabled() our_payment_pubkey = our_conf.payment_basepoint.pubkey - to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey) + to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=chan.has_anchors()) found_to_remote = bool(ctx.get_output_idxs_from_address(to_remote_address)) else: found_to_remote = False @@ -388,6 +403,32 @@ def create_sweeptxs_for_their_ctx( 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 + assert chan.is_static_remotekey_enabled() + 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_abs=0, + gen_tx=sweep_tx) + # HTLCs def create_sweeptx_for_htlc( *, htlc: 'UpdateAddHtlc', @@ -400,9 +441,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_abs=htlc.cltv_abs) + cltv_abs=htlc.cltv_abs, + has_anchors=chan.has_anchors(), + ) cltv_abs = htlc.cltv_abs if is_received_htlc and not is_revocation else 0 + 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, @@ -413,10 +457,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_abs=cltv_abs, - 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_abs=cltv_abs, gen_tx=sweep_tx) # received HTLCs, in their ctx --> "timeout" @@ -481,7 +527,10 @@ 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_abs: int, config: SimpleConfig) -> Optional[PartialTransaction]: + cltv_abs: int, + config: SimpleConfig, + has_anchors: bool, +) -> Optional[PartialTransaction]: assert type(cltv_abs) is int preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered) val = ctx.outputs()[output_idx].value @@ -490,6 +539,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) @@ -511,7 +562,9 @@ 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_bytes(compressed=True) val = ctx.outputs()[output_idx].value prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) @@ -519,15 +572,31 @@ def create_sweeptx_their_ctx_to_remote( txin._trusted_value_sats = val desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey.hex(), script_type='p2wpkh') txin.script_descriptor = desc + 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()}) + + if not has_anchors: + sweep_tx.set_rbf(True) + sweep_tx.sign({our_payment_pubkey: our_payment_privkey.get_secret_bytes()}) + 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 38754c9725f9bfdda4c45bf575c0db94af91c37f Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:46:29 +0200 Subject: [PATCH 08/19] lnwatcher: renaming and comments for clarity --- electrum/lnwatcher.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index 77ba89550d5f..dda57c0590df 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -222,7 +222,7 @@ async def check_onchain_situation(self, address, funding_outpoint): if closing_txid: closing_tx = self.adb.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 @@ -238,7 +238,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, @@ -246,7 +246,7 @@ 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]: """ returns a dict of spenders for a transaction of interest. subscribes to addresses as a side effect. @@ -347,7 +347,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: @@ -434,15 +434,21 @@ async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str await self.lnworker.handle_onchain_state(chan) @log_exceptions - 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 + + # create and broadcast transactions for prevout, sweep_info in sweep_info_dict.items(): name = sweep_info.name + ' ' + chan.get_id_for_log() spender_txid = spenders.get(prevout) From 2f897bc59c1a08882235054cc83199feed37d9d1 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:48:48 +0200 Subject: [PATCH 09/19] lnwatcher: add field for onchain htlc settlement control --- electrum/lnwatcher.py | 6 ++++++ electrum/lnworker.py | 1 + 2 files changed, 7 insertions(+) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index dda57c0590df..f1239e1eb985 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -532,6 +532,12 @@ async def maybe_redeem(self, spenders, prevout, sweep_info: 'SweepInfo', name: s # self.logger.debug( # f"pending redeem for {prevout}. waiting for {name}: CSV " # f"({local_height=}, {wanted_height=}, {prev_height.height=}, {sweep_info.csv_delay=})") + reason = 'waiting for {}: CSV ({} >= {})'.format(name, prev_height.conf, sweep_info.csv_delay) + if not (sweep_info.cltv_abs 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 if can_broadcast: self.logger.info(f'we can broadcast: {name}') tx_was_added = await self.network.try_broadcasting(new_tx, name) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 9b88153031a7..5977d20d6764 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -835,6 +835,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 55d51177f5366a6713c18d355c81147f3bacc953 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Wed, 15 Sep 2021 09:47:58 +0200 Subject: [PATCH 10/19] 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 | 14 +++-- electrum/lnsweep.py | 131 ++++++++++++++++++++++++++++++--------- electrum/lnutil.py | 13 +++- tests/regtest/regtest.sh | 3 +- 4 files changed, 124 insertions(+), 37 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 87cdde899961..1b54ae75cc17 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -54,9 +54,10 @@ 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, - ChannelType, LNProtocolWarning) + ChannelType, LNProtocolWarning, ctx_has_anchors) from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo +from .lnsweep import create_sweeptx_their_backup_ctx from .lnhtlc import HTLCManager from .lnmsg import encode_msg, decode_msg from .address_synchronizer import TX_HEIGHT_LOCAL @@ -578,14 +579,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 b247fee7a3af..03a779ac0d69 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -16,14 +16,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__) @@ -348,6 +349,69 @@ def extract_ctx_secrets(chan: 'Channel', ctx: Transaction): return ctn, their_pcp, is_revocation, per_commitment_secret +def extract_funding_pubkeys_from_ctx(txin: TxInput) -> Tuple[bytes, bytes]: + """Extract the two funding pubkeys from the published commitment transaction. + + We expect to see a witness script of: OP_2 pk1 pk2 OP_2 OP_CHECKMULTISIG""" + elements = txin.witness_elements() + witness_script = elements[-1] + assert match_script_against_template(witness_script, SCRIPT_TEMPLATE_FUNDING) + parsed_script = [x for x in script_GetOp(witness_script)] + pubkey1 = parsed_script[1][1] + pubkey2 = parsed_script[2][1] + return (pubkey1, pubkey2) + + +def create_sweeptx_their_backup_ctx( + *, chan: 'ChannelBackup', + ctx: Transaction, + sweep_address: str) -> Optional[Dict[str, SweepInfo]]: + txs = {} # type: Dict[str, SweepInfo] + """If we only have a backup, and the remote force-closed with their ctx, + and anchors are enabled, we need to sweep to_remote.""" + + if ctx_has_anchors(ctx): + # for anchors we need to sweep to_remote + funding_pubkeys = extract_funding_pubkeys_from_ctx(ctx.inputs()[0]) + _logger.debug(f'checking their ctx for funding pubkeys: {[pk.hex() for pk in funding_pubkeys]}') + # check which of the pubkey was ours + for pubkey in funding_pubkeys: + candidate_basepoint = derive_payment_basepoint(chan.lnworker.static_payment_key.privkey, funding_pubkey=pubkey) + candidate_to_remote_address = make_commitment_output_to_remote_address(candidate_basepoint.pubkey, has_anchors=True) + if ctx.get_output_idxs_from_address(candidate_to_remote_address): + our_payment_pubkey = candidate_basepoint + to_remote_address = candidate_to_remote_address + _logger.debug(f'found funding pubkey') + break + else: + return + else: + # we are dealing with static_remotekey which is locked to a wallet address + return {} + + # to_remote + csv_delay = 1 + our_payment_privkey = ecc.ECPrivkey(our_payment_pubkey.privkey) + output_idxs = ctx.get_output_idxs_from_address(to_remote_address) + if output_idxs: + output_idx = output_idxs.pop() + prevout = ctx.txid() + ':%d' % output_idx + sweep_tx = lambda: create_sweeptx_their_ctx_to_remote( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + our_payment_privkey=our_payment_privkey, + config=chan.lnworker.config, + has_anchors=True + ) + txs[prevout] = SweepInfo( + name='their_ctx_to_remote_backup', + csv_delay=csv_delay, + cltv_abs=0, + gen_tx=sweep_tx) + return txs + + def create_sweeptxs_for_their_ctx( *, chan: 'Channel', ctx: Transaction, @@ -388,6 +452,7 @@ def create_sweeptxs_for_their_ctx( if not found_to_local and not found_to_remote: return chan.logger.debug(f'(lnsweep) found their ctx: {to_local_address} {to_remote_address}') + # 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) @@ -398,38 +463,42 @@ def create_sweeptxs_for_their_ctx( csv_delay=0, cltv_abs=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 - assert chan.is_static_remotekey_enabled() - 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_abs=0, - gen_tx=sweep_tx) + sweep_to_remote = True + our_payment_privkey = ecc.ECPrivkey(our_conf.payment_basepoint.privkey) + else: + assert chan.is_static_remotekey_enabled() + csv_delay = 0 + sweep_to_remote = False + our_payment_privkey = None + + if sweep_to_remote: + assert our_payment_pubkey == our_payment_privkey.get_public_key_bytes(compressed=True) + output_idxs = ctx.get_output_idxs_from_address(to_remote_address) + if output_idxs: + output_idx = output_idxs.pop() + prevout = ctx.txid() + ':%d' % output_idx + 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_abs=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, diff --git a/electrum/lnutil.py b/electrum/lnutil.py index cfaa2fb70bc6..24479ddaaecd 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -20,7 +20,7 @@ from .crypto import sha256, pw_decode_with_version_and_mac from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint, - PartialTxOutput, opcodes, TxOutput) + PartialTxOutput, opcodes, TxOutput, OPPushDataPubkey) from .ecc import CURVE_ORDER, ecdsa_sig64_from_der_sig, ECPubkey, string_to_number from . import ecc, bitcoin, crypto, transaction from . import descriptor @@ -57,6 +57,8 @@ LN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1 DUST_LIMIT_MAX = 1000 +SCRIPT_TEMPLATE_FUNDING = [opcodes.OP_2, OPPushDataPubkey, OPPushDataPubkey, opcodes.OP_2, opcodes.OP_CHECKMULTISIG] + from .json_db import StoredObject, stored_in, stored_as @@ -1269,6 +1271,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/tests/regtest/regtest.sh b/tests/regtest/regtest.sh index f36c15c957df..80016c4e84ba 100755 --- a/tests/regtest/regtest.sh +++ b/tests/regtest/regtest.sh @@ -169,7 +169,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 0f27342742cc85fa448b31eb789b5dd28ce4ba53 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 11 Oct 2021 11:18:33 +0200 Subject: [PATCH 11/19] 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("cloud_no") +class ChanFeatAnchors(ChannelFeature): + def tooltip(self) -> str: + return _("This channel uses anchor outputs.") + def icon(self) -> QIcon: + return read_QIcon("anchor") + + class ChannelFeatureIcons: def __init__(self, features: Sequence['ChannelFeature']): @@ -458,6 +465,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 8f1be3fbdbca9296ae7d5b99f93ad6cf8e59252f Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 13 Sep 2021 14:06:35 +0200 Subject: [PATCH 12/19] unit tests: test anchors in lnpeer and lnchannel * testing of anchor channels is controlled via TEST_ANCHOR_CHANNELS * rewrite tests in test_lnchannel.py --- tests/test_lnchannel.py | 219 ++++++++++++++++++++++++---------------- tests/test_lnpeer.py | 11 +- 2 files changed, 140 insertions(+), 90 deletions(-) diff --git a/tests/test_lnchannel.py b/tests/test_lnchannel.py index ec49d4374b56..c3f4f6d08806 100644 --- a/tests/test_lnchannel.py +++ b/tests/test_lnchannel.py @@ -34,6 +34,7 @@ from electrum import lnutil from electrum import bip32 as bip32_utils from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED, UpdateAddHtlc +from electrum.lnutil import effective_htlc_tx_weight from electrum.logging import console_stderr_handler from electrum.lnchannel import ChannelState from electrum.json_db import StoredDict @@ -41,6 +42,7 @@ from . import ElectrumTestCase +TEST_ANCHOR_CHANNELS = False one_bitcoin_in_msat = bitcoin.COIN * 1000 @@ -48,7 +50,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) @@ -123,7 +125,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) @@ -155,7 +158,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), @@ -164,7 +167,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) @@ -209,8 +212,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 @@ -222,6 +226,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() @@ -232,15 +239,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_abs' : 5, - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': one_bitcoin_in_msat, + 'cltv_abs': 5, + 'timestamp': 0, } # First Alice adds the outgoing HTLC to her local channel's state @@ -262,40 +269,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))) @@ -340,7 +367,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]) @@ -368,10 +395,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 @@ -380,21 +407,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) @@ -432,8 +459,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) @@ -481,7 +508,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) @@ -642,7 +669,7 @@ 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 @@ -656,13 +683,13 @@ def test_DesyncHTLCs(self): alice_idx = alice_channel.add_htlc(htlc).htlc_id bob_idx = bob_channel.receive_htlc(htlc).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 @@ -682,7 +709,7 @@ def test_DesyncHTLCs(self): # 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) @@ -719,10 +746,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_abs' : 5, - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': int(.5 * one_bitcoin_in_msat), + 'cltv_abs': 5, + 'timestamp': 0, } self.alice_channel.add_htlc(htlc_dict) self.bob_channel.receive_htlc(htlc_dict) @@ -758,9 +785,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_abs' : 5, + 'payment_hash': paymentHash, + 'amount_msat': int(3.5 * one_bitcoin_in_msat), + 'cltv_abs': 5, } self.alice_channel.add_htlc(htlc_dict) self.bob_channel.receive_htlc(htlc_dict) @@ -782,10 +809,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_abs' : 5, - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': int(2 * one_bitcoin_in_msat), + 'cltv_abs': 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 @@ -818,38 +845,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_abs' : 5, # also in create_test_channels - 'timestamp' : 0, + 'payment_hash': paymentHash, + 'amount_msat': 1000 * htlc_amt, + 'cltv_abs': 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()) @@ -858,12 +908,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/tests/test_lnpeer.py b/tests/test_lnpeer.py index fa0b8c20638f..a01dcbc27110 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -43,9 +43,16 @@ from electrum.interface import GracefulDisconnect from electrum.simple_config import SimpleConfig -from .test_lnchannel import create_test_channels +from .test_lnchannel import create_test_channels as create_test_channels_anchors from . import ElectrumTestCase +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( @@ -167,6 +174,8 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.features |= LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM self.features |= LnFeatures.OPTION_CHANNEL_TYPE_OPT self.features |= LnFeatures.OPTION_SCID_ALIAS_OPT + self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT + self.config.set_key('enable_anchor_channels', TEST_ANCHOR_CHANNELS) self.pending_payments = defaultdict(asyncio.Future) for chan in chans: chan.lnworker = self From 4e519f46379b0f0b3fe789cb004815bb53e83f3c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 24 Sep 2022 15:19:31 +0200 Subject: [PATCH 13/19] enable anchor outputs via config option --- electrum/lnutil.py | 1 + electrum/lnworker.py | 2 ++ electrum/simple_config.py | 3 ++- tests/regtest/regtest.sh | 1 + tests/test_lnpeer.py | 4 ++-- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 24479ddaaecd..74a0d28c248f 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1567,6 +1567,7 @@ def name_minimal(self): | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ | LnFeatures.OPTION_CHANNEL_TYPE_OPT | LnFeatures.OPTION_CHANNEL_TYPE_REQ | LnFeatures.OPTION_SCID_ALIAS_OPT | LnFeatures.OPTION_SCID_ALIAS_REQ + | LnFeatures.OPTION_ANCHOR_OUTPUTS_OPT | LnFeatures.OPTION_ANCHOR_OUTPUTS_REQ ) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 5977d20d6764..8015402cd423 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -823,6 +823,8 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.payment_secret_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.PAYMENT_SECRET_KEY).privkey Logger.__init__(self) features = LNWALLET_FEATURES + if self.config.ENABLE_ANCHOR_CHANNELS: + features |= LnFeatures.OPTION_ANCHOR_OUTPUTS_OPT if self.config.ACCEPT_ZEROCONF_CHANNELS: features |= LnFeatures.OPTION_ZEROCONF_OPT LNWorker.__init__(self, self.node_keypair, features, config=self.config) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index c7cd8918eb5e..bcc2b3df1b95 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -1202,7 +1202,8 @@ def _default_swapserver_url(self) -> str: # run submarine swap server locally SWAPSERVER_PORT = ConfigVar('swapserver_port', default=5455, type_=int) TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool) - + # anchor outputs channels + ENABLE_ANCHOR_CHANNELS = ConfigVar('enable_anchor_channels', default=False, type_=bool) # zeroconf channels ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool) ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str) diff --git a/tests/regtest/regtest.sh b/tests/regtest/regtest.sh index 80016c4e84ba..cf5b80d6a9ed 100755 --- a/tests/regtest/regtest.sh +++ b/tests/regtest/regtest.sh @@ -89,6 +89,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/tests/test_lnpeer.py b/tests/test_lnpeer.py index a01dcbc27110..bb7df53b3133 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -46,7 +46,7 @@ from .test_lnchannel import create_test_channels as create_test_channels_anchors from . import ElectrumTestCase -TEST_ANCHOR_CHANNELS = False +TEST_ANCHOR_CHANNELS = True def create_test_channels(*args, **kwargs): @@ -175,7 +175,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.features |= LnFeatures.OPTION_CHANNEL_TYPE_OPT self.features |= LnFeatures.OPTION_SCID_ALIAS_OPT self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT - self.config.set_key('enable_anchor_channels', TEST_ANCHOR_CHANNELS) + self.config.ENABLE_ANCHOR_CHANNELS = TEST_ANCHOR_CHANNELS self.pending_payments = defaultdict(asyncio.Future) for chan in chans: chan.lnworker = self From 26e89bcc44dfd44e58038106d15db717f36d0974 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Wed, 15 Sep 2021 10:27:46 +0200 Subject: [PATCH 14/19] regtest: adapt to anchor channels * tests are kept variable via TEST_ANCHOR_CHANNELS --- electrum/lnchannel.py | 7 ++++--- tests/regtest/regtest.sh | 22 ++++++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 1b54ae75cc17..dd9a2dcb3d61 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -869,8 +869,9 @@ def sweep_address(self) -> str: assert self.is_static_remotekey_enabled() our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey addr = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors()) - if self.lnworker: - assert self.lnworker.wallet.is_mine(addr) + # this assert fails with anchor output channels + #if self.lnworker: + # assert self.lnworker.wallet.is_mine(addr) return addr def has_anchors(self) -> bool: @@ -1215,7 +1216,7 @@ def get_remote_htlc_sig_for_htlc(self, *, htlc_relative_idx: int) -> bytes: htlc_sigs = list(chunks(data, 64)) htlc_sig = htlc_sigs[htlc_relative_idx] remote_sighash = Sighash.ALL if not self.has_anchors() else Sighash.ANYONECANPAY | Sighash.SINGLE - remote_htlc_sig = ecc.ecdsa_der_sig_from_ecdsa_sig64(htlc_sig) + remote_sighash.to_sigbytes(1, 'big') + remote_htlc_sig = ecc.ecdsa_der_sig_from_ecdsa_sig64(htlc_sig) + Sighash.to_sigbytes(remote_sighash) return remote_htlc_sig def revoke_current_commitment(self): diff --git a/tests/regtest/regtest.sh b/tests/regtest/regtest.sh index cf5b80d6a9ed..5e90bcf047a1 100755 --- a/tests/regtest/regtest.sh +++ b/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" @@ -89,7 +90,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 @@ -399,8 +400,16 @@ 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 @@ -439,7 +448,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 == "just_in_time" ]]; then From 2c570307f7473d9f7475d031533bee387defd4ed Mon Sep 17 00:00:00 2001 From: bitromortac Date: Fri, 15 Oct 2021 11:08:10 +0200 Subject: [PATCH 15/19] 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/lnchannel.py | 2 +- electrum/lnpeer.py | 2 +- electrum/lnsweep.py | 104 +++++++++++++++++++++++++++++++++++++----- electrum/lnutil.py | 4 +- electrum/lnworker.py | 2 +- electrum/util.py | 5 ++ tests/test_lnutil.py | 3 ++ 7 files changed, 106 insertions(+), 16 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index dd9a2dcb3d61..2ef6dc8d5b6f 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -876,7 +876,7 @@ def sweep_address(self) -> str: def has_anchors(self) -> bool: channel_type = ChannelType(self.storage.get('channel_type')) - return bool(channel_type & ChannelType.OPTION_ANCHOR_OUTPUTS) + return bool(channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX) def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]: assert self.is_static_remotekey_enabled() diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index e33427f45ec2..cbddcdf318d6 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -654,7 +654,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 03a779ac0d69..6dd16f2e866f 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -5,10 +5,11 @@ from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Callable from enum import Enum, auto -from .util import bfh +from .util import bfh, UneconomicFee from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness from .invoices import PR_PAID from . import descriptor +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, @@ -30,6 +31,9 @@ _logger = get_logger(__name__) # note: better to use chan.logger instead, when applicable +HTLC_TRANSACTION_DEADLINE_FRACTION = 4 +HTLC_TRANSACTION_SWEEP_TARGET = 10 + class SweepInfo(NamedTuple): name: str @@ -311,12 +315,15 @@ def create_txns_for_htlc( continue else: preimage = None - create_txns_for_htlc( - htlc=htlc, - htlc_direction=direction, - ctx_output_idx=ctx_output_idx, - htlc_relative_idx=htlc_relative_idx, - preimage=preimage) + try: + create_txns_for_htlc( + htlc=htlc, + htlc_direction=direction, + ctx_output_idx=ctx_output_idx, + htlc_relative_idx=htlc_relative_idx, + preimage=preimage) + except UneconomicFee: + continue return txs @@ -573,7 +580,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_out, htlc_tx = make_htlc_tx_with_open_channel( + witness_script_out, maybe_zero_fee_htlc_tx = make_htlc_tx_with_open_channel( chan=chan, pcp=our_pcp, subject=LOCAL, @@ -583,13 +590,88 @@ 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_{htlc.payment_hash.hex()}') + + # 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_abs + 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) # FIXME + 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 = htlc_tx.sign_txin(0, local_htlc_privkey) - txin = htlc_tx.inputs()[0] + local_htlc_sig = final_htlc_tx.sign_txin(0, local_htlc_privkey) + txin = final_htlc_tx.inputs()[0] witness_script_in = txin.witness_script assert witness_script_in txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_script_in) - return witness_script_out, htlc_tx + return witness_script_out, final_htlc_tx def create_sweeptx_their_ctx_htlc( diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 74a0d28c248f..11844e65d51c 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1066,7 +1066,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 @@ -1567,7 +1567,7 @@ def name_minimal(self): | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ | LnFeatures.OPTION_CHANNEL_TYPE_OPT | LnFeatures.OPTION_CHANNEL_TYPE_REQ | LnFeatures.OPTION_SCID_ALIAS_OPT | LnFeatures.OPTION_SCID_ALIAS_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 8015402cd423..4e3a6d286edd 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -824,7 +824,7 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): Logger.__init__(self) features = LNWALLET_FEATURES if self.config.ENABLE_ANCHOR_CHANNELS: - features |= LnFeatures.OPTION_ANCHOR_OUTPUTS_OPT + features |= LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT if self.config.ACCEPT_ZEROCONF_CHANNELS: features |= LnFeatures.OPTION_ZEROCONF_OPT LNWorker.__init__(self, self.node_keypair, features, config=self.config) diff --git a/electrum/util.py b/electrum/util.py index 63369b755385..4b03a667dec7 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -145,6 +145,11 @@ def __str__(self): return _("Insufficient funds") +class UneconomicFee(Exception): + def __str__(self): + return _("The fee for the transaction is higher than the funds gained from it.") + + class NoDynamicFeeEstimates(Exception): def __str__(self): return _('Dynamic fee estimates not available') diff --git a/tests/test_lnutil.py b/tests/test_lnutil.py index 5b780ec73d77..0e4eacee0c4f 100644 --- a/tests/test_lnutil.py +++ b/tests/test_lnutil.py @@ -796,6 +796,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: From a8d34dbcca472e2b72d0fb6ac5158ea0c24784b1 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 8 Nov 2021 15:26:41 +0100 Subject: [PATCH 16/19] 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/lnutil.py | 3 +- tests/__init__.py | 1 + tests/regtest.py | 21 ++++- tests/regtest/regtest.sh | 2 +- tests/test_lnchannel.py | 60 ++++++++----- tests/test_lnpeer.py | 34 ++++--- tests/test_lnutil.py | 188 ++++++++++++++++++++------------------- 7 files changed, 181 insertions(+), 128 deletions(-) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 11844e65d51c..71dbe023ff56 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1174,7 +1174,8 @@ def make_commitment( 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) + # the following does not hold with anchor outputs + #assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat) # create commitment tx tx = PartialTransaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2) diff --git a/tests/__init__.py b/tests/__init__.py index 8737da94023e..cbb7494d8597 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -28,6 +28,7 @@ class ElectrumTestCase(unittest.IsolatedAsyncioTestCase, Logger): """Base class for our unit tests.""" TESTNET = False + TEST_ANCHOR_CHANNELS = False # maxDiff = None # for debugging # some unit tests are modifying globals... so we run sequentially: diff --git a/tests/regtest.py b/tests/regtest.py index b7a94d64ba45..ee354afd923d 100644 --- a/tests/regtest.py +++ b/tests/regtest.py @@ -4,10 +4,17 @@ import subprocess class TestLightning(unittest.TestCase): - - @staticmethod - def run_shell(args, timeout=30): - process = subprocess.Popen(['tests/regtest/regtest.sh'] + args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, universal_newlines=True) + TEST_ANCHOR_CHANNELS = False + + @classmethod + def run_shell(cls, args, timeout=30): + process = subprocess.Popen( + ['tests/regtest/regtest.sh'] + args, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, + universal_newlines=True, + env=os.environ.update({'TEST_ANCHOR_CHANNELS': str(cls.TEST_ANCHOR_CHANNELS)}), + ) for line in iter(process.stdout.readline, ''): sys.stdout.write(line) sys.stdout.flush() @@ -75,6 +82,9 @@ def test_breach_with_spent_htlc(self): self.run_shell(['breach_with_spent_htlc']) +class TestLightningABAnchors(TestLightningAB): + TEST_ANCHOR_CHANNELS = True + class TestLightningSwapserver(TestLightning): agents = { 'alice': { @@ -113,6 +123,9 @@ class TestLightningWatchtower(TestLightning): def test_watchtower(self): self.run_shell(['watchtower']) +class TestLightningWatchtowerAnchors(TestLightningWatchtower): + TEST_ANCHOR_CHANNELS = True + class TestLightningJIT(TestLightning): agents = { diff --git a/tests/regtest/regtest.sh b/tests/regtest/regtest.sh index 5e90bcf047a1..85b0193a9a2b 100755 --- a/tests/regtest/regtest.sh +++ b/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" @@ -86,6 +85,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/tests/test_lnchannel.py b/tests/test_lnchannel.py index c3f4f6d08806..83a7ceaab29b 100644 --- a/tests/test_lnchannel.py +++ b/tests/test_lnchannel.py @@ -42,17 +42,20 @@ 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_type = lnutil.ChannelType.OPTION_STATIC_REMOTEKEY + if anchor_outputs: + channel_type |= lnutil.ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX + channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index) state = { "channel_id":channel_id.hex(), @@ -112,7 +115,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator, 'log': {}, 'unfulfilled_htlcs': {}, 'revocation_store': {}, - 'channel_type': lnutil.ChannelType.OPTION_STATIC_REMOTEKEY + 'channel_type': channel_type } return StoredDict(state, None, []) @@ -126,7 +129,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) @@ -212,10 +215,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 @@ -227,7 +231,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): @@ -239,7 +243,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) @@ -562,7 +566,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) @@ -666,9 +669,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)) @@ -714,9 +721,13 @@ def test_DesyncHTLCs(self): alice_channel.add_htlc(htlc) +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 @@ -843,10 +854,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) @@ -856,7 +872,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 @@ -878,13 +894,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) @@ -895,12 +911,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/tests/test_lnpeer.py b/tests/test_lnpeer.py index bb7df53b3133..c29164f74535 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -43,14 +43,11 @@ from electrum.interface import GracefulDisconnect from electrum.simple_config import SimpleConfig -from .test_lnchannel import create_test_channels as create_test_channels_anchors -from . import ElectrumTestCase - -TEST_ANCHOR_CHANNELS = True -def create_test_channels(*args, **kwargs): - return create_test_channels_anchors(*args, **kwargs, anchor_outputs=TEST_ANCHOR_CHANNELS) +from .test_lnchannel import create_test_channels +from .test_bitcoin import needs_test_with_all_chacha20_implementations +from . import ElectrumTestCase def keypair(): @@ -149,7 +146,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): MPP_SPLIT_PART_FRACTION = 1 # this disables the forced splitting MPP_SPLIT_PART_MINAMT_MSAT = 5_000_000 - 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) @@ -175,7 +172,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.features |= LnFeatures.OPTION_CHANNEL_TYPE_OPT self.features |= LnFeatures.OPTION_SCID_ALIAS_OPT self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT - self.config.ENABLE_ANCHOR_CHANNELS = TEST_ANCHOR_CHANNELS + self.config.ENABLE_ANCHOR_CHANNELS = has_anchors self.pending_payments = defaultdict(asyncio.Future) for chan in chans: chan.lnworker = self @@ -550,8 +547,8 @@ def prepare_peers( bob_channel.storage['node_id'] = bob_channel.node_id 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) @@ -1420,7 +1417,6 @@ def prepare_chans_and_peers_in_graph(self, graph_definition) -> Graph: transports = {} workers = {} # type: Dict[str, MockLNWallet] peers = {} - # create channels for a, definition in graph_definition.items(): for b, channel_def in definition.get('channels', {}).items(): @@ -1431,6 +1427,7 @@ def prepare_chans_and_peers_in_graph(self, graph_definition) -> Graph: bob_pubkey=keys[b].pubkey, local_msat=channel_def['local_balance_msat'], remote_msat=channel_def['remote_balance_msat'], + anchor_outputs=self.TEST_ANCHOR_CHANNELS ) channels[(a, b)], channels[(b, a)] = channel_ab, channel_ba transport_ab, transport_ba = transport_pair(keys[a], keys[b], channel_ab.name, channel_ba.name) @@ -1444,7 +1441,7 @@ def prepare_chans_and_peers_in_graph(self, graph_definition) -> Graph: # create workers and peers for a, definition in graph_definition.items(): channels_of_node = [c for k, c in channels.items() if k[0] == a] - workers[a] = MockLNWallet(local_keypair=keys[a], chans=channels_of_node, tx_queue=txs_queues[a], name=a) + workers[a] = MockLNWallet(local_keypair=keys[a], chans=channels_of_node, tx_queue=txs_queues[a], name=a, has_anchors=self.TEST_ANCHOR_CHANNELS) self._lnworkers_created.extend(list(workers.values())) # create peers @@ -1481,6 +1478,9 @@ def prepare_chans_and_peers_in_graph(self, graph_definition) -> Graph: print(f" {keys[a].pubkey.hex()}") return graph + async def test_payment_multihop(self): + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) + async def test_payment_multihop(self): graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) peers = graph.peers.values() @@ -1957,3 +1957,13 @@ async def test_payment_trampoline_e2e_indirect(self): with self.assertRaises(PaymentDone): graph = self.create_square_graph(direct=False, is_legacy=False) await self._run_trampoline_payment(graph) + +class TestPeerDirectAnchors(TestPeerDirect): + TEST_ANCHOR_CHANNELS = True + +class TestPeerForwardingAnchors(TestPeerForwarding): + TEST_ANCHOR_CHANNELS = True + + +def run(coro): + return asyncio.run_coroutine_threadsafe(coro, loop=util.get_asyncio_loop()).result() diff --git a/tests/test_lnutil.py b/tests/test_lnutil.py index 0e4eacee0c4f..405f1ed73919 100644 --- a/tests/test_lnutil.py +++ b/tests/test_lnutil.py @@ -796,100 +796,108 @@ 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 = bfh(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( - revocation_pubkey=local_revocation_pubkey, - remote_htlcpubkey=remote_htlcpubkey, - local_htlcpubkey=local_htlcpubkey, + # 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 = bfh(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( + revocation_pubkey=local_revocation_pubkey, + remote_htlcpubkey=remote_htlcpubkey, + local_htlcpubkey=local_htlcpubkey, + payment_hash=bitcoin.sha256(bfh(test_htlc['preimage'])), + cltv_abs=test_htlc['expiry'], + has_anchors=True) + else: + htlc_script = make_offered_htlc( + revocation_pubkey=local_revocation_pubkey, + remote_htlcpubkey=remote_htlcpubkey, + local_htlcpubkey=local_htlcpubkey, + payment_hash=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_abs=test_htlc['expiry'], - has_anchors=True) - else: - htlc_script = make_offered_htlc( - revocation_pubkey=local_revocation_pubkey, - remote_htlcpubkey=remote_htlcpubkey, - local_htlcpubkey=local_htlcpubkey, - payment_hash=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_abs=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_abs))} - 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_abs=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 + 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_abs))} + 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_abs=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: bytes, remote_signature: bytes, pubkey: bytes, privkey: bytes): assert type(remote_pubkey) is bytes From cb8c16debe7286a30025d309e0151b71688b67e3 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Fri, 12 Nov 2021 10:34:50 +0100 Subject: [PATCH 17/19] sweep: rename sweep creation functions naming scheme: tx(s)_our/their_ctx/htlctx_output-description function names are shortened to whether a single (tx) or several sweep transactions (txs) are generated --- electrum/lnchannel.py | 20 +++++------ electrum/lnsweep.py | 79 ++++++++++++++++++++++++------------------- 2 files changed, 54 insertions(+), 45 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 2ef6dc8d5b6f..5af92cd95744 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -55,9 +55,9 @@ fee_for_htlc_output, offered_htlc_trim_threshold_sat, received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT, ChannelType, LNProtocolWarning, 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 @@ -284,10 +284,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 @@ -579,11 +579,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 @@ -1464,9 +1464,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) @@ -1701,7 +1701,7 @@ def get_close_options(self) -> Sequence[ChanCloseOption]: 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 6dd16f2e866f..d701be43c8ca 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -42,8 +42,8 @@ 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]: +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). @@ -65,7 +65,7 @@ def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commit 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_tx = tx_ctx_to_local( sweep_address=sweep_address, ctx=ctx, output_idx=output_idx, @@ -76,7 +76,7 @@ def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commit if sweep_tx: txs.append(sweep_tx) # HTLCs - def create_sweeptx_for_htlc(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction, + 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, @@ -87,7 +87,7 @@ def create_sweeptx_for_htlc(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction, commit=ctx, htlc=htlc, ctx_output_idx=ctx_output_idx) - return create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( + return tx_sweep_our_htlctx( htlc_tx=htlc_tx, htlctx_witness_script=htlc_tx_witness_script, sweep_address=sweep_address, @@ -102,7 +102,7 @@ def create_sweeptx_for_htlc(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction, 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( + secondstage_sweep_tx = txs_their_htlctx_justice( htlc=htlc, htlc_direction=direction, ctx_output_idx=ctx_output_idx) @@ -111,7 +111,7 @@ def create_sweeptx_for_htlc(*, htlc: 'UpdateAddHtlc', htlc_direction: Direction, return txs -def create_sweeptx_for_their_revoked_ctx( +def tx_their_ctx_justice( chan: 'Channel', ctx: Transaction, per_commitment_secret: bytes, @@ -132,7 +132,7 @@ def create_sweeptx_for_their_revoked_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, @@ -144,7 +144,7 @@ def create_sweeptx_for_their_revoked_ctx( return None -def create_sweeptx_for_their_revoked_htlc( +def tx_their_htlctx_justice( chan: 'Channel', ctx: Transaction, htlc_tx: Transaction, @@ -172,7 +172,7 @@ def create_sweeptx_for_their_revoked_htlc( # 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( + gen_tx = lambda: tx_sweep_our_htlctx( sweep_address=sweep_address, htlc_tx=htlc_tx, htlctx_witness_script=witness_script, @@ -186,7 +186,7 @@ def create_sweeptx_for_their_revoked_htlc( gen_tx=gen_tx) -def create_sweeptxs_for_our_ctx( +def txs_our_ctx( *, chan: 'AbstractChannel', ctx: Transaction, sweep_address: str) -> Optional[Dict[str, SweepInfo]]: @@ -240,7 +240,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, @@ -262,13 +262,13 @@ 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, + htlc_relative_idx, preimage: Optional[bytes]): - 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, @@ -278,7 +278,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, @@ -316,7 +316,7 @@ def create_txns_for_htlc( else: preimage = None try: - create_txns_for_htlc( + txs_htlc( htlc=htlc, htlc_direction=direction, ctx_output_idx=ctx_output_idx, @@ -369,10 +369,10 @@ def extract_funding_pubkeys_from_ctx(txin: TxInput) -> Tuple[bytes, bytes]: return (pubkey1, pubkey2) -def create_sweeptx_their_backup_ctx( - *, chan: 'ChannelBackup', - ctx: Transaction, - sweep_address: str) -> Optional[Dict[str, SweepInfo]]: +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.""" @@ -403,7 +403,7 @@ def create_sweeptx_their_backup_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, @@ -419,7 +419,9 @@ def create_sweeptx_their_backup_ctx( return txs -def create_sweeptxs_for_their_ctx( + + +def txs_their_ctx( *, chan: 'Channel', ctx: Transaction, sweep_address: str) -> Optional[Dict[str,SweepInfo]]: @@ -459,10 +461,11 @@ def create_sweeptxs_for_their_ctx( if not found_to_local and not found_to_remote: return chan.logger.debug(f'(lnsweep) found their ctx: {to_local_address} {to_remote_address}') + # 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( @@ -471,6 +474,7 @@ def create_sweeptxs_for_their_ctx( cltv_abs=0, gen_tx=gen_tx) + # to_remote if chan.has_anchors(): csv_delay = 1 @@ -488,7 +492,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, @@ -506,7 +510,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, @@ -524,7 +528,7 @@ def create_sweeptx_for_htlc( cltv_abs = htlc.cltv_abs if is_received_htlc and not is_revocation 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, @@ -559,7 +563,7 @@ def create_sweeptx_for_htlc( continue else: preimage = None - create_sweeptx_for_htlc( + tx_htlc( htlc=htlc, is_received_htlc=is_received_htlc, ctx_output_idx=ctx_output_idx, @@ -567,7 +571,7 @@ def create_sweeptx_for_htlc( return txs -def create_htlctx_that_spends_from_our_ctx( +def tx_our_ctx_htlctx( chan: 'Channel', our_pcp: bytes, ctx: Transaction, @@ -674,7 +678,7 @@ def fee_estimator(size): return witness_script_out, final_htlc_tx -def create_sweeptx_their_ctx_htlc( +def tx_their_ctx_htlc( ctx: Transaction, witness_script: bytes, sweep_address: str, preimage: Optional[bytes], output_idx: int, privkey: bytes, is_revocation: bool, @@ -710,7 +714,8 @@ def create_sweeptx_their_ctx_htlc( return tx -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, @@ -753,7 +758,10 @@ def create_sweeptx_their_ctx_to_remote( return sweep_tx -def create_sweeptx_ctx_to_local( + + + +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]: @@ -786,16 +794,17 @@ def create_sweeptx_ctx_to_local( return sweep_tx -def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_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 second stage htlc tx + """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 create_sweeptx_ctx_to_local( + return tx_ctx_to_local( sweep_address=sweep_address, ctx=htlc_tx, output_idx=0, From a64feb1fc30af05e4382f6b9f6c23ba4bbdcf7b0 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 15 Nov 2021 14:23:33 +0100 Subject: [PATCH 18/19] 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 | 94 ++++++++++++++++++++++++++----------------- electrum/lnwatcher.py | 37 +++++++++++------ 3 files changed, 87 insertions(+), 55 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 5af92cd95744..77bfc198f814 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -56,7 +56,7 @@ received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT, ChannelType, LNProtocolWarning, 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 @@ -587,8 +587,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 @@ -1699,9 +1699,8 @@ def get_close_options(self) -> Sequence[ChanCloseOption]: assert not (self.get_state() == ChannelState.WE_ARE_TOXIC and ChanCloseOption.LOCAL_FCLOSE in ret), "local force-close unsafe if we are toxic" return ret - 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 d701be43c8ca..0ed12e0e3dae 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -33,6 +33,7 @@ HTLC_TRANSACTION_DEADLINE_FRACTION = 4 HTLC_TRANSACTION_SWEEP_TARGET = 10 +HTLCTX_INPUT_OUTPUT_INDEX = 0 class SweepInfo(NamedTuple): @@ -87,8 +88,9 @@ 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, @@ -144,19 +146,23 @@ 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( @@ -164,26 +170,37 @@ 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 = 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=witness_script, - privkey=other_revocation_privkey, - is_revocation=True, - config=chan.lnworker.config) - return SweepInfo( - name='redeem_htlc2', - csv_delay=0, - cltv_abs=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=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_abs=0, + gen_tx=gen_tx + ) + + return index_to_sweepinfo def txs_our_ctx( @@ -278,21 +295,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_abs=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_abs=0, @@ -637,16 +658,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, @@ -670,8 +691,8 @@ 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 = final_htlc_tx.sign_txin(0, local_htlc_privkey) - txin = final_htlc_tx.inputs()[0] + local_htlc_sig = final_htlc_tx.sign_txin(HTLCTX_INPUT_OUTPUT_INDEX, local_htlc_privkey) + txin = final_htlc_tx.inputs()[HTLCTX_INPUT_OUTPUT_INDEX] witness_script_in = txin.witness_script assert witness_script_in txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_script_in) @@ -686,6 +707,7 @@ def tx_their_ctx_htlc( config: SimpleConfig, has_anchors: bool, ) -> Optional[PartialTransaction]: + """Deals with normal (non-CSV timelocked) HTLC output sweeps.""" assert type(cltv_abs) is int preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered) val = ctx.outputs()[output_idx].value @@ -795,8 +817,8 @@ def tx_ctx_to_local( -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 @@ -807,7 +829,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, diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index f1239e1eb985..5e2fcfced4fa 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -445,7 +445,8 @@ 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 @@ -453,22 +454,32 @@ async def sweep_commitment_transaction(self, funding_outpoint, closing_tx, spend name = sweep_info.name + ' ' + chan.get_id_for_log() spender_txid = spenders.get(prevout) spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None + if spender_tx: + + # 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) + # the spender might be the remote, revoked or not - e_htlc_tx = chan.maybe_sweep_revoked_htlc(closing_tx, spender_tx) - if e_htlc_tx: - spender2 = spenders.get(spender_txid+':0') - if spender2: - keep_watching |= not self.is_deeply_mined(spender2) + htlc_idx_to_sweepinfo = chan.maybe_sweep_revoked_htlcs(closing_tx, spender_tx) + for idx, htlc_revocation_sweep_info in htlc_idx_to_sweepinfo.items(): + htlc_tx_spender = spenders.get(spender_txid+f':{idx}') + if htlc_tx_spender: + keep_watching |= not self.is_deeply_mined(htlc_tx_spender) else: keep_watching = True - await self.maybe_redeem(spenders, spender_txid+':0', e_htlc_tx, name) - else: - keep_watching |= not self.is_deeply_mined(spender_tx.txid()) - txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout)) - assert txin_idx is not None - spender_txin = spender_tx.inputs()[txin_idx] - chan.extract_preimage_from_htlc_txin(spender_txin) + await self.maybe_redeem(spenders, spender_txid+f':{idx}', htlc_revocation_sweep_info, name) else: keep_watching = True # broadcast or maybe update our own tx From dfba48392500d6d0781b9bd22eaeeb774bb49f84 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 15 Nov 2021 14:27:16 +0100 Subject: [PATCH 19/19] 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 | 81 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 0ed12e0e3dae..38f40610d108 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -52,16 +52,18 @@ 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 = 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: @@ -71,14 +73,67 @@ def txs_their_ctx_watchtower(chan: 'Channel', ctx: Transaction, per_commitment_s ctx=ctx, output_idx=output_idx, witness_script=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_abs=htlc.cltv_abs, + has_anchors=chan.has_anchors() + ) + + cltv_abs = htlc.cltv_abs 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_abs=cltv_abs, + 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, @@ -93,7 +148,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)