diff --git a/electrum/gui/icons/anchor.png b/electrum/gui/icons/anchor.png new file mode 100644 index 000000000000..20b152fe813b Binary files /dev/null and b/electrum/gui/icons/anchor.png differ diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 257256cf17ca..8a8726218292 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -438,6 +438,13 @@ def icon(self) -> 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: diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 18c5156cabcd..77bfc198f814 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -53,10 +53,11 @@ ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script, ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr, fee_for_htlc_output, offered_htlc_trim_threshold_sat, - received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, - 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 + 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 txs_their_htlctx_justice, SweepInfo +from .lnsweep import tx_their_ctx_to_remote_backup from .lnhtlc import HTLCManager from .lnmsg import encode_msg, decode_msg from .address_synchronizer import TX_HEIGHT_LOCAL @@ -283,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 @@ -398,7 +399,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 @@ -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): """ @@ -518,8 +522,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, @@ -570,14 +579,19 @@ def is_backup(self): return True def create_sweeptxs_for_their_ctx(self, ctx): - return {} + return tx_their_ctx_to_remote_backup(chan=self, ctx=ctx, sweep_address=self.sweep_address) def create_sweeptxs_for_our_ctx(self, ctx): if self.is_imported: - return create_sweeptxs_for_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address) + return txs_our_ctx(chan=self, ctx=ctx, sweep_address=self.sweep_address) else: - # backup from op_return - return {} + return + + def maybe_sweep_revoked_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[int, SweepInfo]: + return {} + + def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None: + return None def get_funding_address(self): return self.cb.funding_address @@ -616,6 +630,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) @@ -851,15 +868,20 @@ 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) - if self.lnworker: - assert self.lnworker.wallet.is_mine(addr) + addr = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors()) + # this assert fails with anchor output channels + #if self.lnworker: + # 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_ANCHORS_ZERO_FEE_HTLC_TX) + 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: @@ -1099,6 +1121,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)) @@ -1167,6 +1193,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) @@ -1186,7 +1215,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) + Sighash.to_sigbytes(remote_sighash) return remote_htlc_sig def revoke_current_commitment(self): @@ -1292,7 +1322,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 @@ -1313,28 +1343,47 @@ def consider_ctx(*, ctx_owner: HTLCOwner, is_htlc_dust: bool) -> int: feerate=feerate, is_local_initiator=self.constraints.is_initiator, round_to_sat=False, + has_anchors=self.has_anchors() ) htlc_fee_msat = fee_for_htlc_output(feerate=feerate) htlc_trim_func = received_htlc_trim_threshold_sat if ctx_owner == receiver else offered_htlc_trim_threshold_sat - htlc_trim_threshold_msat = htlc_trim_func(dust_limit_sat=self.config[ctx_owner].dust_limit_sat, feerate=feerate) * 1000 - if sender == initiator == LOCAL: # see https://github.com/lightningnetwork/lightning-rfc/pull/740 + htlc_trim_threshold_msat = htlc_trim_func(dust_limit_sat=self.config[ctx_owner].dust_limit_sat, feerate=feerate, has_anchors=self.has_anchors()) * 1000 + + # the sender cannot spend below its reserve + max_send_msat = sender_balance_msat - sender_reserve_msat + + # reserve a fee spike buffer + # see https://github.com/lightningnetwork/lightning-rfc/pull/740 + if sender == initiator == LOCAL: fee_spike_buffer = calc_fees_for_commitment_tx( num_htlcs=num_htlcs_in_ctx + int(not is_htlc_dust) + 1, feerate=2 * feerate, is_local_initiator=self.constraints.is_initiator, round_to_sat=False, - )[sender] - max_send_msat = sender_balance_msat - sender_reserve_msat - fee_spike_buffer - else: - max_send_msat = sender_balance_msat - sender_reserve_msat - ctx_fees_msat[sender] + has_anchors=self.has_anchors())[sender] + max_send_msat -= fee_spike_buffer + # we can't enforce the fee spike buffer on the remote party + elif sender == initiator == REMOTE: + max_send_msat -= ctx_fees_msat[sender] + + # initiator pays for anchor outputs + if sender == initiator and self.has_anchors(): + max_send_msat -= 2 * FIXED_ANCHOR_SAT + + # handle the transaction fees for the HTLC transaction if is_htlc_dust: + # nobody pays additional HTLC transaction fees return min(max_send_msat, htlc_trim_threshold_msat - 1) else: + # somebody has to pay for the additonal HTLC transaction fees if sender == initiator: return max_send_msat - htlc_fee_msat else: - # the receiver is the initiator, so they need to be able to pay tx fees - if receiver_balance_msat - receiver_reserve_msat - ctx_fees_msat[receiver] - htlc_fee_msat < 0: + # check if the receiver can afford to pay for the HTLC transaction fees + new_receiver_balance = receiver_balance_msat - receiver_reserve_msat - ctx_fees_msat[receiver] - htlc_fee_msat + if self.has_anchors(): + new_receiver_balance -= 2 * FIXED_ANCHOR_SAT + if new_receiver_balance < 0: return 0 return max_send_msat @@ -1353,7 +1402,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). """ @@ -1365,9 +1414,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)) @@ -1415,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) @@ -1505,6 +1554,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: @@ -1547,13 +1597,15 @@ def make_commitment(self, subject: HTLCOwner, this_point: bytes, ctn: int) -> Pa remote_htlc_pubkey=other_htlc_pubkey, local_htlc_pubkey=this_htlc_pubkey, payment_hash=htlc.payment_hash, - cltv_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( 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 @@ -1575,22 +1627,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, @@ -1642,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 create_sweeptx_for_their_revoked_htlc(self, ctx, htlc_tx, self.sweep_address) + def maybe_sweep_revoked_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[int, SweepInfo]: + return txs_their_htlctx_justice(self, ctx, htlc_tx, self.sweep_address) def has_pending_changes(self, subject: HTLCOwner) -> bool: next_htlcs = self.hm.get_htlcs_in_next_ctx(subject) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index f1639f75b08a..cbddcdf318d6 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_ANCHORS_ZERO_FEE_HTLC_OPT) + def upfront_shutdown_script_from_payload(self, payload, msg_identifier: str) -> Optional[bytes]: if msg_identifier not in ['accept', 'open']: raise ValueError("msg_identifier must be either 'accept' or 'open'") @@ -675,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) @@ -690,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, @@ -866,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 @@ -1036,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/lnsweep.py b/electrum/lnsweep.py index e4744468b5b2..38f40610d108 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, @@ -16,19 +17,24 @@ LOCAL, REMOTE, make_htlc_output_witness_script, get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed, RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED, - map_htlcs_to_ctx_output_idxs, Direction) -from .transaction import (Transaction, TxOutput, PartialTransaction, PartialTxInput, - PartialTxOutput, TxOutpoint) + map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script, + derive_payment_basepoint, ctx_has_anchors, SCRIPT_TEMPLATE_FUNDING) +from .transaction import (Transaction, TxInput, PartialTransaction, PartialTxInput, + PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template) from .simple_config import SimpleConfig from .logging import get_logger, Logger if TYPE_CHECKING: - from .lnchannel import Channel, AbstractChannel + from .lnchannel import Channel, AbstractChannel, ChannelBackup _logger = get_logger(__name__) # note: better to use chan.logger instead, when applicable +HTLC_TRANSACTION_DEADLINE_FRACTION = 4 +HTLC_TRANSACTION_SWEEP_TARGET = 10 +HTLCTX_INPUT_OUTPUT_INDEX = 0 + class SweepInfo(NamedTuple): name: str @@ -37,8 +43,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). @@ -46,33 +52,88 @@ def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commit # 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: 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, 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 create_sweeptx_for_htlc(*, 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, @@ -82,11 +143,12 @@ 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_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, + privkey=watcher_revocation_privkey, is_revocation=True, config=chan.lnworker.config) @@ -97,7 +159,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) @@ -106,7 +168,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, @@ -127,7 +189,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, @@ -139,19 +201,23 @@ def create_sweeptx_for_their_revoked_ctx( return None -def create_sweeptx_for_their_revoked_htlc( +def txs_their_htlctx_justice( chan: 'Channel', ctx: Transaction, htlc_tx: Transaction, - sweep_address: str) -> Optional[SweepInfo]: - - x = analyze_ctx(chan, ctx) + 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( @@ -159,36 +225,55 @@ def create_sweeptx_for_their_revoked_htlc( 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: create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( - 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) - - -def create_sweeptxs_for_our_ctx( + # 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( *, 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 +297,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 @@ -227,7 +312,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, @@ -249,13 +334,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, @@ -265,21 +350,25 @@ def create_txns_for_htlc( htlc_direction=htlc_direction, ctx_output_idx=ctx_output_idx, htlc_relative_idx=htlc_relative_idx) - sweep_tx = lambda: create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( + # we sweep our ctx with HTLC transactions individually, therefore the CSV-locked output is always at + # index TIMELOCKED_HTLCTX_OUTPUT_INDEX + assert True + sweep_tx = lambda: tx_sweep_htlctx_output( to_self_delay=to_self_delay, htlc_tx=htlc_tx, + output_idx=HTLCTX_INPUT_OUTPUT_INDEX, htlctx_witness_script=htlctx_witness_script, sweep_address=sweep_address, privkey=our_localdelayed_privkey.get_secret_bytes(), is_revocation=False, config=chan.lnworker.config) # side effect - txs[htlc_tx.inputs()[0].prevout.to_str()] = SweepInfo( + txs[htlc_tx.inputs()[HTLCTX_INPUT_OUTPUT_INDEX].prevout.to_str()] = SweepInfo( name='first-stage-htlc', csv_delay=0, cltv_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, @@ -302,16 +391,19 @@ 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: + txs_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 -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) @@ -340,21 +432,93 @@ def analyze_ctx(chan: 'Channel', ctx: Transaction): return ctn, their_pcp, is_revocation, per_commitment_secret -def create_sweeptxs_for_their_ctx( +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 tx_their_ctx_to_remote_backup( + *, chan: 'ChannelBackup', + ctx: Transaction, + sweep_address: str) -> Optional[Dict[str, SweepInfo]]: + txs = {} # type: Dict[str, SweepInfo] + """If we only have a backup, and the remote force-closed with their ctx, + and anchors are enabled, we need to sweep to_remote.""" + + if ctx_has_anchors(ctx): + # for anchors we need to sweep to_remote + funding_pubkeys = extract_funding_pubkeys_from_ctx(ctx.inputs()[0]) + _logger.debug(f'checking their ctx for funding pubkeys: {[pk.hex() for pk in funding_pubkeys]}') + # check which of the pubkey was ours + for pubkey in funding_pubkeys: + candidate_basepoint = derive_payment_basepoint(chan.lnworker.static_payment_key.privkey, funding_pubkey=pubkey) + candidate_to_remote_address = make_commitment_output_to_remote_address(candidate_basepoint.pubkey, has_anchors=True) + if ctx.get_output_idxs_from_address(candidate_to_remote_address): + our_payment_pubkey = candidate_basepoint + to_remote_address = candidate_to_remote_address + _logger.debug(f'found funding pubkey') + break + else: + return + else: + # we are dealing with static_remotekey which is locked to a wallet address + return {} + + # to_remote + csv_delay = 1 + our_payment_privkey = ecc.ECPrivkey(our_payment_pubkey.privkey) + output_idxs = ctx.get_output_idxs_from_address(to_remote_address) + if output_idxs: + output_idx = output_idxs.pop() + prevout = ctx.txid() + ':%d' % output_idx + sweep_tx = lambda: tx_their_ctx_to_remote( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + our_payment_privkey=our_payment_privkey, + config=chan.lnworker.config, + has_anchors=True + ) + txs[prevout] = SweepInfo( + name='their_ctx_to_remote_backup', + csv_delay=csv_delay, + cltv_abs=0, + gen_tx=sweep_tx) + return txs + + + + +def txs_their_ctx( *, chan: 'Channel', ctx: Transaction, sweep_address: str) -> Optional[Dict[str,SweepInfo]]: - """Handle the case when the remote force-closes with their ctx. - Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs). - Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher. + """Handle the case where the remote force-closes with their ctx. + + We sweep: + to_local: if revoked + to_remote: CSV delay with anchors, otherwise sweeping not needed + htlc success: CSV delay with anchors, no delay otherwise, or revoked + htlc timeout: CSV delay with anchors, CLTV locktime, or revoked + second-stage htlc transactions: CSV delay + + Outputs with CSV/CLTV are redeemed by LNWatcher. """ txs = {} # type: Dict[str, SweepInfo] our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True) - x = analyze_ctx(chan, ctx) + x = extract_ctx_secrets(chan, ctx) if not x: return ctn, their_pcp, is_revocation, per_commitment_secret = x - # to_local and to_remote addresses + # to_local our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp) their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp) witness_script = make_commitment_output_to_local_witness_script( @@ -366,16 +530,18 @@ 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 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( @@ -383,13 +549,44 @@ def create_sweeptxs_for_their_ctx( csv_delay=0, cltv_abs=0, gen_tx=gen_tx) - # prep + + + # to_remote + if chan.has_anchors(): + csv_delay = 1 + 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: tx_their_ctx_to_remote( + sweep_address=sweep_address, + ctx=ctx, + output_idx=output_idx, + our_payment_privkey=our_payment_privkey, + config=chan.lnworker.config, + has_anchors=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) - # to_local is handled by lnwatcher - # HTLCs - def create_sweeptx_for_htlc( + def tx_htlc( *, htlc: 'UpdateAddHtlc', is_received_htlc: bool, ctx_output_idx: int, @@ -400,11 +597,14 @@ def create_sweeptx_for_htlc( remote_htlc_pubkey=our_htlc_privkey.get_public_key_bytes(compressed=True), local_htlc_pubkey=their_htlc_pubkey, payment_hash=htlc.payment_hash, - cltv_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( + sweep_tx = lambda: tx_their_ctx_htlc( ctx=ctx, witness_script=htlc_output_witness_script, sweep_address=sweep_address, @@ -413,10 +613,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" @@ -437,7 +639,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, @@ -445,7 +647,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, @@ -458,7 +660,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, @@ -468,20 +670,99 @@ 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()[HTLCTX_INPUT_OUTPUT_INDEX].address + htlc_output_idx = funded_htlc_tx.get_output_idxs_from_address(htlc_out_address).pop() + inputs = funded_htlc_tx.inputs() + outputs = funded_htlc_tx.outputs() + if htlc_input_idx != HTLCTX_INPUT_OUTPUT_INDEX: + htlc_txin = inputs.pop(htlc_input_idx) + inputs.insert(HTLCTX_INPUT_OUTPUT_INDEX, htlc_txin) + if htlc_output_idx != HTLCTX_INPUT_OUTPUT_INDEX: + htlc_txout = outputs.pop(htlc_output_idx) + outputs.insert(HTLCTX_INPUT_OUTPUT_INDEX, htlc_txout) + final_htlc_tx = PartialTransaction.from_io( + inputs, + outputs, + locktime=maybe_zero_fee_htlc_tx.locktime, + version=maybe_zero_fee_htlc_tx.version, + BIP69_sort=False + ) + + for fee_input_idx in range(1, len(funded_htlc_tx.inputs())): + txin = final_htlc_tx.inputs()[fee_input_idx] + pubkey = wallet.get_public_key(txin.address) + index = wallet.get_address_index(txin.address) + privkey, _ = wallet.keystore.get_private_key(index, chan.lnworker.wallet_password) # 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(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) - return witness_script_out, htlc_tx + 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, - cltv_abs: int, config: SimpleConfig) -> Optional[PartialTransaction]: + cltv_abs: int, + 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 @@ -490,6 +771,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) @@ -508,10 +791,13 @@ 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) -> 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,21 +805,40 @@ 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 -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]: @@ -566,19 +871,20 @@ def create_sweeptx_ctx_to_local( return sweep_tx -def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx( - *, 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 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, + output_idx=output_idx, witness_script=htlctx_witness_script, privkey=privkey, is_revocation=is_revocation, diff --git a/electrum/lnutil.py b/electrum/lnutil.py index fbf34a2b87d8..71dbe023ff56 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 @@ -46,13 +46,19 @@ # defined in BOLT-03: HTLC_TIMEOUT_WEIGHT = 663 +HTLC_TIMEOUT_WEIGHT_ANCHORS = 666 HTLC_SUCCESS_WEIGHT = 703 +HTLC_SUCCESS_WEIGHT_ANCHORS = 706 COMMITMENT_TX_WEIGHT = 724 +COMMITMENT_TX_WEIGHT_ANCHORS = 1124 HTLC_OUTPUT_WEIGHT = 172 +FIXED_ANCHOR_SAT = 330 LN_MAX_FUNDING_SAT_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 @@ -158,6 +164,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 +190,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") @@ -212,7 +221,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 +228,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,8 +611,24 @@ 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, + 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 @@ -603,7 +639,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 @@ -645,12 +681,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), @@ -676,8 +713,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( @@ -687,12 +727,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), @@ -721,8 +762,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 = [ @@ -795,18 +839,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], @@ -833,6 +884,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) @@ -885,12 +937,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, @@ -898,11 +952,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) @@ -943,41 +1000,90 @@ 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=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 offered_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int) -> int: +def effective_htlc_tx_weight(success: bool, has_anchors: bool): + # for anchors-zero-fee-htlc we set an effective weight of zero + # we only trim htlcs below dust, as in the anchors commitment format, + # the fees for the hltc transaction don't need to be subtracted from + # the htlc output, but fees are taken from extra attached inputs + if has_anchors: + return 0 * HTLC_SUCCESS_WEIGHT_ANCHORS if success else 0 * HTLC_TIMEOUT_WEIGHT_ANCHORS + else: + return HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT + + +def offered_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int, has_anchors: bool) -> int: # offered htlcs strictly below this amount will be trimmed (from ctx). # feerate is in sat/kw # returns value in sat - weight = HTLC_TIMEOUT_WEIGHT + weight = effective_htlc_tx_weight(success=False, has_anchors=has_anchors) return dust_limit_sat + weight * feerate // 1000 -def received_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int) -> int: +def received_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int, has_anchors: bool) -> int: # received htlcs strictly below this amount will be trimmed (from ctx). # feerate is in sat/kw # returns value in sat - weight = HTLC_SUCCESS_WEIGHT + weight = effective_htlc_tx_weight(success=True, has_anchors=has_anchors) return dust_limit_sat + weight * feerate // 1000 @@ -988,13 +1094,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 @@ -1022,7 +1132,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) @@ -1035,7 +1146,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 @@ -1052,9 +1168,14 @@ 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) + # 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) @@ -1084,8 +1205,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) + 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) def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config): tx.sign({local_config.multisig_key.pubkey: local_config.multisig_key.privkey}) @@ -1120,6 +1272,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()) @@ -1190,10 +1351,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) @@ -1390,6 +1568,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_ANCHORS_ZERO_FEE_HTLC_OPT | LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_REQ ) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index 77ba89550d5f..5e2fcfced4fa 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,35 +434,52 @@ 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 - sweep_info_dict = chan.sweep_ctx(closing_tx) + # detect who closed and get information about how to claim outputs + sweep_info_dict = chan.sweep_ctx(closing_tx) # output -> SweepInfo + # spenders: output -> txid keep_watching = False if sweep_info_dict else not self.is_deeply_mined(closing_tx.txid()) - # create and broadcast transaction + + # 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) 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 @@ -526,6 +543,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 2b4e24df33d8..4e3a6d286edd 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -819,9 +819,12 @@ 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 + if self.config.ENABLE_ANCHOR_CHANNELS: + 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) @@ -834,6 +837,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! 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/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/__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/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/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 f36c15c957df..85b0193a9a2b 100755 --- a/tests/regtest/regtest.sh +++ b/tests/regtest/regtest.sh @@ -85,10 +85,12 @@ if [[ $1 == "new_block" ]]; then fi if [[ $1 == "init" ]]; then + echo "testing anchor channels: $TEST_ANCHOR_CHANNELS" echo "initializing $2" rm -rf /tmp/$2/ agent="./run_electrum --regtest -D /tmp/$2" $agent create --offline > /dev/null + $agent setconfig --offline enable_anchor_channels $TEST_ANCHOR_CHANNELS $agent setconfig --offline log_to_file True $agent setconfig --offline use_gossip True $agent setconfig --offline server 127.0.0.1:51001:t @@ -169,7 +171,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 @@ -397,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 @@ -437,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 diff --git a/tests/test_lnchannel.py b/tests/test_lnchannel.py index ec49d4374b56..83a7ceaab29b 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,16 +42,20 @@ from . import ElectrumTestCase - one_bitcoin_in_msat = bitcoin.COIN * 1000 def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, - r_csv): + r_csv, anchor_outputs): #assert local_amount > 0 #assert remote_amount > 0 + + channel_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(), @@ -110,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, []) @@ -123,7 +128,8 @@ def bip32(sequence): def create_test_channels(*, feerate=6000, local_msat=None, remote_msat=None, alice_name="alice", bob_name="bob", - alice_pubkey=b"\x01"*33, bob_pubkey=b"\x02"*33, random_seed=None): + alice_pubkey=b"\x01"*33, bob_pubkey=b"\x02"*33, random_seed=None, + anchor_outputs=False): if random_seed is None: # needed for deterministic randomness random_seed = os.urandom(32) random_gen = PRNG(random_seed) @@ -155,7 +161,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 +170,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 +215,10 @@ class TestFee(ElectrumTestCase): def test_fee(self): alice_channel, bob_channel = create_test_channels(feerate=253, local_msat=10000000000, - remote_msat=5000000000) - self.assertIn(9999817, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()]) + remote_msat=5000000000, anchor_outputs=self.TEST_ANCHOR_CHANNELS) + expected_value = 9999056 if self.TEST_ANCHOR_CHANNELS else 9999817 + self.assertIn(expected_value, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()]) + class TestChannel(ElectrumTestCase): maxDiff = 999 @@ -222,6 +230,9 @@ def assertOutputExistsByValue(self, tx, amt_sat): else: self.assertFalse() + def assertNumberNonAnchorOutputs(self, number, tx): + self.assertEqual(number, len(tx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + @classmethod def setUpClass(cls): super().setUpClass() @@ -232,15 +243,15 @@ def setUp(self): # Create a test channel which will be used for the duration of this # unittest. The channel will be funded evenly with Alice having 5 BTC, # and Bob having 5 BTC. - self.alice_channel, self.bob_channel = create_test_channels() + self.alice_channel, self.bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) self.paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(self.paymentPreimage) self.htlc_dict = { - 'payment_hash' : paymentHash, - 'amount_msat' : one_bitcoin_in_msat, - 'cltv_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 +273,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 +371,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 +399,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 +411,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 +463,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 +512,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) @@ -535,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) @@ -639,10 +669,14 @@ def test_AddHTLCNegativeBalance(self): self.assertIn('Not enough local balance', cm.exception.args[0]) +class TestChannelAnchors(TestChannel): + TEST_ANCHOR_CHANNELS = True + + class TestAvailableToSpend(ElectrumTestCase): def test_DesyncHTLCs(self): - alice_channel, bob_channel = create_test_channels() - self.assertEqual(499986152000, alice_channel.available_to_spend(LOCAL)) + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) + self.assertEqual(499986152000 if not alice_channel.has_anchors() else 499981351340, alice_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) paymentPreimage = b"\x01" * 32 @@ -656,13 +690,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,14 +716,18 @@ 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) +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 @@ -719,10 +757,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 +796,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 +820,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 @@ -816,40 +854,72 @@ def check_bals(self, amt1, amt2): self.assertEqual(self.alice_channel.available_to_spend(REMOTE), amt2) self.assertEqual(self.bob_channel.available_to_spend(LOCAL), amt2) + +class TestChanReserveAnchors(TestChanReserve): + TEST_ANCHOR_CHANNELS = True + + class TestDust(ElectrumTestCase): def test_DustLimit(self): - alice_channel, bob_channel = create_test_channels() + """Test that addition of an HTLC below the dust limit changes the balances.""" + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) + dust_limit_alice = alice_channel.config[LOCAL].dust_limit_sat + dust_limit_bob = bob_channel.config[LOCAL].dust_limit_sat + self.assertLess(dust_limit_alice, dust_limit_bob) + bob_ctx = bob_channel.get_latest_commitment(LOCAL) + bobs_original_outputs = [x.value for x in bob_ctx.outputs()] paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(paymentPreimage) fee_per_kw = alice_channel.get_next_feerate(LOCAL) - self.assertEqual(fee_per_kw, 6000) - htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000) - self.assertEqual(htlcAmt, 4478) + success_weight = effective_htlc_tx_weight(success=True, has_anchors=self.TEST_ANCHOR_CHANNELS) + # we put a single sat less into the htlc than bob can afford + # to pay for his htlc success transaction + below_dust_for_bob = dust_limit_bob - 1 + htlc_amt = below_dust_for_bob + success_weight * (fee_per_kw // 1000) htlc = { - 'payment_hash' : paymentHash, - 'amount_msat' : 1000 * htlcAmt, - 'cltv_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 self.TEST_ANCHOR_CHANNELS else 0)) + # htlc in bob's case goes to miner fees + self.assertEqual(2, len(bob_ctx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(htlc_amt, sum(bobs_original_outputs) - sum(bobs_second_outputs)) + empty_ctx_fee = lnutil.calc_fees_for_commitment_tx( + num_htlcs=0, feerate=fee_per_kw, is_local_initiator=True, + round_to_sat=True, has_anchors=self.TEST_ANCHOR_CHANNELS)[LOCAL] // 1000 + self.assertEqual(empty_ctx_fee + htlc_amt, bob_channel.get_next_fee(LOCAL)) + + bob_channel.settle_htlc(paymentPreimage, bob_htlc_id) + alice_channel.receive_htlc_settle(paymentPreimage, alice_htlc_id) force_state_transition(bob_channel, alice_channel) - self.assertEqual(len(alice_channel.get_next_commitment(LOCAL).outputs()), 2) - self.assertEqual(alice_channel.total_msat(SENT) // 1000, htlcAmt) + bob_ctx = bob_channel.get_latest_commitment(LOCAL) + bobs_third_outputs = [x.value for x in bob_ctx.outputs()] + # htlc is added back into the balance + self.assertEqual(sum(bobs_original_outputs), sum(bobs_third_outputs)) + # balance shifts in bob's direction after settlement + self.assertEqual(htlc_amt, bobs_third_outputs[1 + (2 if self.TEST_ANCHOR_CHANNELS else 0)] - bobs_original_outputs[1 + (2 if self.TEST_ANCHOR_CHANNELS else 0)]) + self.assertEqual(2, len(alice_channel.get_next_commitment(LOCAL).outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(2, len(bob_channel.get_next_commitment(LOCAL).outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(htlc_amt, alice_channel.total_msat(SENT) // 1000) + + +class TestDustAnchors(TestDust): + TEST_ANCHOR_CHANNELS = True + def force_state_transition(chanA, chanB): chanB.receive_new_commitment(*chanA.sign_next_commitment()) @@ -858,12 +928,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..c29164f74535 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -43,9 +43,13 @@ from electrum.interface import GracefulDisconnect from electrum.simple_config import SimpleConfig + + from .test_lnchannel import create_test_channels +from .test_bitcoin import needs_test_with_all_chacha20_implementations from . import ElectrumTestCase + def keypair(): priv = ECPrivkey.generate_random_key().get_secret_bytes() k1 = Keypair( @@ -142,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) @@ -167,6 +171,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.ENABLE_ANCHOR_CHANNELS = has_anchors self.pending_payments = defaultdict(asyncio.Future) for chan in chans: chan.lnworker = self @@ -541,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) @@ -1411,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(): @@ -1422,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) @@ -1435,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 @@ -1472,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() @@ -1948,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 1d05cbc5d24c..405f1ed73919 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 @@ -11,6 +13,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 @@ -19,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 @@ -42,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 = [ @@ -490,23 +536,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" @@ -536,8 +582,10 @@ def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self): local_amount=to_local_msat, remote_amount=to_remote_msat, dust_limit_sat=local_dust_limit_satoshi, - fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=len(htlcs), feerate=local_feerate_per_kw, is_local_initiator=True), - htlcs=htlcs) + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=len(htlcs), feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), + htlcs=htlcs, + has_anchors=False + ) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) self.assertEqual(str(our_commit_tx), output_commit_tx) @@ -564,22 +612,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 +649,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) @@ -623,8 +688,10 @@ def test_commitment_tx_with_one_output(self): local_amount=to_local_msat, remote_amount=to_remote_msat, dust_limit_sat=local_dust_limit_satoshi, - fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True), - htlcs=[]) + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), + htlcs=[], + has_anchors=False + ) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) self.assertEqual(str(our_commit_tx), output_commit_tx) @@ -652,8 +719,10 @@ def test_commitment_tx_with_fee_greater_than_funder_amount(self): local_amount=to_local_msat, remote_amount=to_remote_msat, dust_limit_sat=local_dust_limit_satoshi, - fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True), - htlcs=[]) + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), + htlcs=[], + has_anchors=False + ) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) self.assertEqual(str(our_commit_tx), output_commit_tx) @@ -719,12 +788,117 @@ def test_simple_commitment_tx_with_no_HTLCs(self): local_amount=to_local_msat, remote_amount=to_remote_msat, dust_limit_sat=local_dust_limit_satoshi, - fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True), - htlcs=[]) + fees_per_participant=calc_fees_for_commitment_tx(num_htlcs=0, feerate=local_feerate_per_kw, is_local_initiator=True, has_anchors=False), + htlcs=[], + has_anchors=False + ) self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey) ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220' self.assertEqual(str(our_commit_tx), ref_commit_tx_str) + @disable_ecdsa_r_value_grinding + def test_commitment_tx_anchors_test_vectors(self): + # this test is only valid for the original anchor output test vectors (not anchors-zero-fee-htlcs), + # therefore we patch the effective htlc tx weight to result in a finite weight + from electrum import lnutil + effective_htlc_tx_weight_original = lnutil.effective_htlc_tx_weight + def effective_htlc_tx_weight_patched(success: bool, has_anchors: bool): + return lnutil.HTLC_SUCCESS_WEIGHT_ANCHORS if success else lnutil.HTLC_TIMEOUT_WEIGHT_ANCHORS + lnutil.effective_htlc_tx_weight = effective_htlc_tx_weight_patched + + try: + for test_vector in ANCHOR_TEST_VECTORS: + with self.subTest(test_vector['Name']): + to_local_msat = test_vector['LocalBalance'] + to_remote_msat = test_vector['RemoteBalance'] + local_feerate_per_kw = test_vector['FeePerKw'] + ref_commit_tx_str = test_vector['ExpectedCommitmentTxHex'] + remote_signature = 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 + 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 assert len(remote_pubkey) == 33