diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 04b14071c..19395ab61 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -55,7 +55,7 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script, ShortChannelID, map_htlcs_to_ctx_output_idxs, fee_for_htlc_output, offered_htlc_trim_threshold_sat, - received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, + 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 @@ -888,7 +888,7 @@ class Channel(AbstractChannel): # TODO: in case of unilateral close with pending HTLCs, this address will be reused 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 @@ -900,7 +900,7 @@ class Channel(AbstractChannel): 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: @@ -1341,7 +1341,7 @@ class Channel(AbstractChannel): 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 @@ -1367,25 +1367,42 @@ class Channel(AbstractChannel): 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 * 1000 + + # 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 * 1000 + if new_receiver_balance < 0: return 0 return max_send_msat @@ -1629,22 +1646,27 @@ class Channel(AbstractChannel): 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 aee6390d7..6057a3a15 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -52,6 +52,7 @@ HTLC_SUCCESS_WEIGHT_ANCHORS = 706 COMMITMENT_TX_WEIGHT = 724 COMMITMENT_TX_WEIGHT_ANCHORS = 1124 HTLC_OUTPUT_WEIGHT = 172 +FIXED_ANCHOR_SAT = 330 LN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1 DUST_LIMIT_MAX = 1000 @@ -992,26 +993,64 @@ RECEIVED = Direction.RECEIVED 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=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=local_anchor_script, value=FIXED_ANCHOR_SAT)) + if to_remote_amt_msat // 1000 >= dust_limit_sat or htlc_outputs: + 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=local_script, value=to_local_amt_msat // 1000)) + non_htlc_outputs.append(PartialTxOutput(scriptpubkey=remote_script, value=to_remote_amt_msat // 1000)) - # trim outputs c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs)) - return htlc_outputs, c_outputs_filtered + c_outputs = c_outputs_filtered + anchor_outputs + return htlc_outputs, c_outputs def effective_htlc_tx_weight(success: bool, has_anchors: bool): @@ -1086,7 +1125,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) @@ -1099,7 +1139,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 @@ -1116,7 +1161,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) @@ -1148,8 +1197,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 = 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 = 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 4d4b8dcc9..01bbf24f6 100644 --- a/tests/test_lnutil.py +++ b/tests/test_lnutil.py @@ -539,7 +539,9 @@ class TestLNUtil(ElectrumTestCase): 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) @@ -643,7 +645,9 @@ class TestLNUtil(ElectrumTestCase): 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) @@ -672,7 +676,9 @@ class TestLNUtil(ElectrumTestCase): 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) @@ -739,7 +745,9 @@ class TestLNUtil(ElectrumTestCase): 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)