diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index c3f6cd3bc..bf48259dc 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -415,6 +415,10 @@ class AbstractChannel(Logger, ABC): def get_funding_address(self) -> str: pass + def get_funding_tx(self) -> Optional[Transaction]: + funding_txid = self.funding_outpoint.txid + return self.lnworker.lnwatcher.adb.get_transaction(funding_txid) + @abstractmethod def get_sweep_address(self) -> str: """Returns a wallet address we can use to sweep coins to. @@ -534,6 +538,12 @@ class ChannelBackup(AbstractChannel): self.logger.warning( f"local_payment_pubkey missing from (old-type) channel backup. " f"You should export and re-import a newer backup.") + multisig_funding_keypair = None + if multisig_funding_secret := cb.multisig_funding_privkey: + multisig_funding_keypair = Keypair( + privkey=multisig_funding_secret, + pubkey=ecc.ECPrivkey(multisig_funding_secret).get_public_key_bytes(), + ) self.config[LOCAL] = LocalConfig.from_seed( channel_seed=cb.channel_seed, to_self_delay=cb.local_delay, @@ -542,6 +552,7 @@ class ChannelBackup(AbstractChannel): # 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, + multisig_key=multisig_funding_keypair, # dummy values static_payment_key=None, dust_limit_sat=None, @@ -594,7 +605,9 @@ class ChannelBackup(AbstractChannel): return True def create_sweeptxs_for_their_ctx(self, ctx): - return sweep_their_ctx_to_remote_backup(chan=self, ctx=ctx) + funding_tx = self.get_funding_tx() + assert funding_tx + return sweep_their_ctx_to_remote_backup(chan=self, ctx=ctx, funding_tx=funding_tx) def create_sweeptxs_for_our_ctx(self, ctx): if self.is_imported: diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 691659c33..ce674d14a 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -46,7 +46,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, ChannelConf IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage, ChannelType, LNProtocolWarning, validate_features, IncompatibleOrInsaneFeatures) from .lnutil import FeeUpdate, channel_id_from_funding_tx, PaymentFeeBudget -from .lnutil import serialize_htlc_key +from .lnutil import serialize_htlc_key, Keypair from .lntransport import LNTransport, LNTransportBase, LightningPeerConnectionClosed, HandshakeFailed from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg from .interface import GracefulDisconnect @@ -673,7 +673,15 @@ class Peer(Logger, EventListener): self.logger.info(f"upfront shutdown script received: {upfront_shutdown_script}") return upfront_shutdown_script - def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner, channel_type: ChannelType) -> LocalConfig: + def make_local_config( + self, + *, + funding_sat: int, + push_msat: int, + initiator: HTLCOwner, + channel_type: ChannelType, + multisig_funding_keypair: Optional[Keypair], # if None, will get derived from channel_seed + ) -> LocalConfig: channel_seed = os.urandom(32) initial_msat = funding_sat * 1000 - push_msat if initiator == LOCAL else push_msat @@ -692,6 +700,14 @@ class Peer(Logger, EventListener): static_payment_key = None static_remotekey = bytes.fromhex(wallet.get_public_key(addr)) + if multisig_funding_keypair: + for chan in self.lnworker.channels.values(): # check against all chans of lnworker, for sanity + if multisig_funding_keypair.pubkey == chan.config[LOCAL].multisig_key.pubkey: + raise Exception( + "Refusing to reuse multisig_funding_keypair for new channel. " + "Wait one block before opening another channel with this peer." + ) + dust_limit_sat = bitcoin.DUST_LIMIT_P2PKH reserve_sat = max(funding_sat // 100, dust_limit_sat) # for comparison of defaults, see @@ -702,6 +718,7 @@ class Peer(Logger, EventListener): channel_seed=channel_seed, static_remotekey=static_remotekey, static_payment_key=static_payment_key, + multisig_key=multisig_funding_keypair, upfront_shutdown_script=upfront_shutdown_script, to_self_delay=self.network.config.LIGHTNING_TO_SELF_DELAY_CSV, dust_limit_sat=dust_limit_sat, @@ -787,7 +804,21 @@ class Peer(Logger, EventListener): 'type': our_channel_type.to_bytes_minimal() } - local_config = self.make_local_config(funding_sat, push_msat, LOCAL, our_channel_type) + if self.use_anchors(): + multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_we_opened( + funding_root_secret=self.lnworker.funding_root_keypair.privkey, + remote_node_id_or_prefix=self.pubkey, + nlocktime=funding_tx.locktime, + ) + else: + multisig_funding_keypair = None + local_config = self.make_local_config( + funding_sat=funding_sat, + push_msat=push_msat, + initiator=LOCAL, + channel_type=our_channel_type, + multisig_funding_keypair=multisig_funding_keypair, + ) # if it includes open_channel_tlvs: MUST include upfront_shutdown_script. open_channel_tlvs['upfront_shutdown_script'] = { 'shutdown_scriptpubkey': local_config.upfront_shutdown_script @@ -1019,7 +1050,21 @@ class Peer(Logger, EventListener): if not channel_type.complies_with_features(self.features): raise Exception("sender has sent a channel type we don't support") - local_config = self.make_local_config(funding_sat, push_msat, REMOTE, channel_type) + if self.use_anchors(): + multisig_funding_keypair = lnutil.derive_multisig_funding_key_if_they_opened( + funding_root_secret=self.lnworker.funding_root_keypair.privkey, + remote_node_id_or_prefix=self.pubkey, + remote_funding_pubkey=payload['funding_pubkey'], + ) + else: + multisig_funding_keypair = None + local_config = self.make_local_config( + funding_sat=funding_sat, + push_msat=push_msat, + initiator=REMOTE, + channel_type=channel_type, + multisig_funding_keypair=multisig_funding_keypair, + ) upfront_shutdown_script = self.upfront_shutdown_script_from_payload( payload, 'open') diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index b88ea833c..30bcb05e5 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -19,7 +19,8 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o get_ordered_channel_configs, get_per_commitment_secret_from_seed, RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED, map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script, - derive_payment_basepoint, ctx_has_anchors, SCRIPT_TEMPLATE_FUNDING) + derive_payment_basepoint, ctx_has_anchors, SCRIPT_TEMPLATE_FUNDING, Keypair, + derive_multisig_funding_key_if_we_opened, derive_multisig_funding_key_if_they_opened) from .transaction import (Transaction, TxInput, PartialTxInput, PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template) from .simple_config import SimpleConfig @@ -483,7 +484,9 @@ def extract_funding_pubkeys_from_ctx(txin: TxInput) -> Tuple[bytes, bytes]: def sweep_their_ctx_to_remote_backup( *, chan: 'ChannelBackup', - ctx: Transaction) -> Optional[Dict[str, SweepInfo]]: + ctx: Transaction, + funding_tx: Transaction, +) -> 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.""" @@ -493,7 +496,7 @@ def sweep_their_ctx_to_remote_backup( 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: + for fp_idx, pubkey in enumerate(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): @@ -508,8 +511,33 @@ def sweep_their_ctx_to_remote_backup( return {} # remote anchor + # derive funding_privkey ("multisig_key") + # note: for imported backups, we already have this as 'local_config.multisig_key' + # but for on-chain backups, we need to derive it. + # For symmetry, we derive it now regardless of type + our_funding_pubkey = funding_pubkeys[fp_idx] + their_funding_pubkey = funding_pubkeys[1 - fp_idx] + remote_node_id = chan.node_id # for onchain backups, this is only the prefix + if chan.is_initiator(): + funding_kp_cand = derive_multisig_funding_key_if_we_opened( + funding_root_secret=chan.lnworker.funding_root_keypair.privkey, + remote_node_id_or_prefix=remote_node_id, + nlocktime=funding_tx.locktime, + ) + else: + funding_kp_cand = derive_multisig_funding_key_if_they_opened( + funding_root_secret=chan.lnworker.funding_root_keypair.privkey, + remote_node_id_or_prefix=remote_node_id, + remote_funding_pubkey=their_funding_pubkey, + ) + assert funding_kp_cand.pubkey == our_funding_pubkey, f"funding pubkey mismatch1. {chan.is_initiator()=}" + our_ms_funding_keypair = funding_kp_cand + # sanity check funding_privkey, if we had it already (if backup is imported): if local_config := chan.config.get(LOCAL): - if txin := sweep_ctx_anchor(ctx=ctx, multisig_key=local_config.multisig_key): + assert our_ms_funding_keypair == local_config.multisig_key, f"funding pubkey mismatch2. {chan.is_initiator()=}" + + if our_ms_funding_keypair: + if txin := sweep_ctx_anchor(ctx=ctx, multisig_key=our_ms_funding_keypair): txs[txin.prevout.to_str()] = SweepInfo( name='remote_anchor', csv_delay=0, @@ -517,9 +545,6 @@ def sweep_their_ctx_to_remote_backup( txin=txin, txout=None, ) - else: - # fixme: onchain channel backups do not store the channel seed - pass # to_remote csv_delay = 1 @@ -810,7 +835,7 @@ def sweep_their_ctx_to_remote( return txin -def sweep_ctx_anchor(*, ctx: Transaction, multisig_key)-> Optional[PartialTxInput]: +def sweep_ctx_anchor(*, ctx: Transaction, multisig_key: Keypair) -> Optional[PartialTxInput]: from .lnutil import make_commitment_output_to_anchor_address, make_commitment_output_to_anchor_witness_script local_funding_pubkey = multisig_key.pubkey local_anchor_address = make_commitment_output_to_anchor_address(local_funding_pubkey) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 61c7e3789..3f64f9f00 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -224,7 +224,8 @@ class LocalConfig(ChannelConfig): 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 - kwargs['multisig_key'] = keypair_generator(LnKeyFamily.MULTISIG) + if kwargs['multisig_key'] is None: + kwargs['multisig_key'] = keypair_generator(LnKeyFamily.MULTISIG) kwargs['htlc_basepoint'] = keypair_generator(LnKeyFamily.HTLC_BASE) kwargs['delayed_basepoint'] = keypair_generator(LnKeyFamily.DELAY_BASE) kwargs['revocation_basepoint'] = keypair_generator(LnKeyFamily.REVOCATION_BASE) @@ -279,8 +280,8 @@ class ChannelConstraints(StoredObject): funding_txn_minimum_depth = attr.ib(type=int) -CHANNEL_BACKUP_VERSION_LATEST = 1 -KNOWN_CHANNEL_BACKUP_VERSIONS = (0, 1,) +CHANNEL_BACKUP_VERSION_LATEST = 2 +KNOWN_CHANNEL_BACKUP_VERSIONS = (0, 1, 2, ) assert CHANNEL_BACKUP_VERSION_LATEST in KNOWN_CHANNEL_BACKUP_VERSIONS @attr.s @@ -315,6 +316,7 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) remote_revocation_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) local_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes] + multisig_funding_privkey = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes] def to_bytes(self) -> bytes: vds = BCDataStream() @@ -333,6 +335,7 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): vds.write_string(self.host) vds.write_uint16(self.port) vds.write_bytes(self.local_payment_pubkey, 33) + vds.write_bytes(self.multisig_funding_privkey, 32) return bytes(vds.input) @staticmethod @@ -359,6 +362,10 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): local_payment_pubkey = vds.read_bytes(33) else: local_payment_pubkey = None + if version >= 2: + multisig_funding_privkey = vds.read_bytes(32) + else: + multisig_funding_privkey = None return ImportedChannelBackupStorage( is_initiator=is_initiator, privkey=privkey, @@ -374,6 +381,7 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): host=host, port=port, local_payment_pubkey=local_payment_pubkey, + multisig_funding_privkey=multisig_funding_privkey, ) @staticmethod @@ -616,6 +624,54 @@ def derive_payment_basepoint(static_payment_secret: bytes, funding_pubkey: bytes ) +def derive_multisig_funding_key_if_we_opened( + *, + funding_root_secret: bytes, + remote_node_id_or_prefix: bytes, + nlocktime: int, +) -> Keypair: + from .lnworker import NODE_ID_PREFIX_LEN + assert isinstance(funding_root_secret, bytes) + assert len(funding_root_secret) == 32 + assert isinstance(remote_node_id_or_prefix, bytes) + assert len(remote_node_id_or_prefix) in (NODE_ID_PREFIX_LEN, 33) + assert isinstance(nlocktime, int) + nlocktime_bytes = int.to_bytes(nlocktime, length=4, byteorder="little", signed=False) + node_id_prefix = remote_node_id_or_prefix[0:NODE_ID_PREFIX_LEN] + funding_key = ecc.ECPrivkey(bitcoin.bip340_tagged_hash( + tag=b"electrum/ln_multisig_funding_key/we_opened", + msg=funding_root_secret + node_id_prefix + nlocktime_bytes, + )) + return Keypair( + pubkey=funding_key.get_public_key_bytes(), + privkey=funding_key.get_secret_bytes(), + ) + + +def derive_multisig_funding_key_if_they_opened( + *, + funding_root_secret: bytes, + remote_node_id_or_prefix: bytes, + remote_funding_pubkey: bytes, +) -> Keypair: + from .lnworker import NODE_ID_PREFIX_LEN + assert isinstance(funding_root_secret, bytes) + assert len(funding_root_secret) == 32 + assert isinstance(remote_node_id_or_prefix, bytes) + assert len(remote_node_id_or_prefix) in (NODE_ID_PREFIX_LEN, 33) + assert isinstance(remote_funding_pubkey, bytes) + assert len(remote_funding_pubkey) == 33 + node_id_prefix = remote_node_id_or_prefix[0:NODE_ID_PREFIX_LEN] + funding_key = ecc.ECPrivkey(bitcoin.bip340_tagged_hash( + tag=b"electrum/ln_multisig_funding_key/they_opened", + msg=funding_root_secret + node_id_prefix + remote_funding_pubkey, + )) + return Keypair( + pubkey=funding_key.get_public_key_bytes(), + privkey=funding_key.get_secret_bytes(), + ) + + def make_htlc_tx_output( amount_msat, local_feerate, @@ -1693,6 +1749,7 @@ class LnKeyFamily(IntEnum): BACKUP_CIPHER = 7 | BIP32_PRIME PAYMENT_SECRET_KEY = 8 | BIP32_PRIME NOSTR_KEY = 9 | BIP32_PRIME + FUNDING_ROOT_KEY = 10 | BIP32_PRIME def generate_keypair(node: BIP32Node, key_family: LnKeyFamily) -> Keypair: diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index 47f445f8f..b0078af1d 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -132,7 +132,7 @@ class LNWatcher(Logger, EventListener): if not keep_watching: await self.unwatch_channel(address, funding_outpoint) - async def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool: + async def sweep_commitment_transaction(self, funding_outpoint: str, closing_tx: Transaction) -> bool: raise NotImplementedError() # implemented by subclasses async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str, diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 782c29e27..43c9a5d7a 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -103,6 +103,7 @@ NUM_PEERS_TARGET = 4 # onchain channel backup data CB_VERSION = 0 CB_MAGIC_BYTES = bytes([0, 0, 0, CB_VERSION]) +NODE_ID_PREFIX_LEN = 16 FALLBACK_NODE_LIST_TESTNET = ( @@ -846,6 +847,7 @@ class LNWallet(LNWorker): 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 + self.funding_root_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.FUNDING_ROOT_KEY) Logger.__init__(self) features = LNWALLET_FEATURES if self.config.ENABLE_ANCHOR_CHANNELS: @@ -1368,8 +1370,8 @@ class LNWallet(LNWorker): self.remove_channel(chan.channel_id) raise - def cb_data(self, node_id): - return CB_MAGIC_BYTES + node_id[0:16] + def cb_data(self, node_id: bytes) -> bytes: + return CB_MAGIC_BYTES + node_id[0:NODE_ID_PREFIX_LEN] def decrypt_cb_data(self, encrypted_data, funding_address): funding_scripthash = bytes.fromhex(address_to_scripthash(funding_address)) @@ -1389,6 +1391,8 @@ class LNWallet(LNWorker): funding_sat: int, node_id: bytes, fee_est=None) -> PartialTransaction: + from .wallet import get_locktime_for_new_transaction + outputs = [PartialTxOutput.from_address_and_value(DummyAddress.CHANNEL, funding_sat)] if self.has_recoverable_channels(): dummy_scriptpubkey = make_op_return(self.cb_data(node_id)) @@ -1398,6 +1402,9 @@ class LNWallet(LNWorker): outputs=outputs, fee=fee_est) tx.set_rbf(False) + # rm randomness from locktime, as we use the locktime as entropy for deriving the funding_privkey + # (and it would be confusing to get a collision as a consequence of the randomness) + tx.locktime = get_locktime_for_new_transaction(self.network, include_random_component=False) return tx def suggest_funding_amount(self, amount_to_pay, coins): @@ -2979,6 +2986,7 @@ class LNWallet(LNWorker): remote_revocation_pubkey = chan.config[REMOTE].revocation_basepoint.pubkey, remote_payment_pubkey = chan.config[REMOTE].payment_basepoint.pubkey, local_payment_pubkey=chan.config[LOCAL].payment_basepoint.pubkey, + multisig_funding_privkey=chan.config[LOCAL].multisig_key.privkey, ) def export_channel_backup(self, channel_id): @@ -3114,7 +3122,7 @@ class LNWallet(LNWorker): encrypted_data = o2.scriptpubkey[2:] data = self.decrypt_cb_data(encrypted_data, funding_address) if data.startswith(CB_MAGIC_BYTES): - node_id_prefix = data[4:] + node_id_prefix = data[len(CB_MAGIC_BYTES):] if node_id_prefix is None: return funding_txid = tx.txid() diff --git a/electrum/wallet.py b/electrum/wallet.py index 8c1fb1e99..2af3a0e25 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -206,7 +206,11 @@ async def sweep( return tx -def get_locktime_for_new_transaction(network: 'Network') -> int: +def get_locktime_for_new_transaction( + network: 'Network', + *, + include_random_component: bool = True, +) -> int: # if no network or not up to date, just set locktime to zero if not network: return 0 @@ -225,8 +229,9 @@ def get_locktime_for_new_transaction(network: 'Network') -> int: locktime = min(chain_height, server_height) # sometimes pick locktime a bit further back, to help privacy # of setups that need more time (offline/multisig/coinjoin/...) - if random.randint(0, 9) == 0: - locktime = max(0, locktime - random.randint(0, 99)) + if include_random_component: + if random.randint(0, 9) == 0: + locktime = max(0, locktime - random.randint(0, 99)) locktime = max(0, locktime) return locktime diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index 390662504..474e96306 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -72,7 +72,7 @@ class WalletUnfinished(WalletFileException): # seed_version is now used for the version of the wallet file OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 59 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 60 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format @@ -234,6 +234,7 @@ class WalletDBUpgrader(Logger): self._convert_version_57() self._convert_version_58() self._convert_version_59() + self._convert_version_60() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure def _convert_wallet_type(self): @@ -1146,6 +1147,15 @@ class WalletDBUpgrader(Logger): self.data['channels'] = channels self.data['seed_version'] = 59 + def _convert_version_60(self): + if not self._is_upgrade_method_needed(59, 59): + return + cbs = self.data.get('imported_channel_backups', {}) + for channel_id, cb in list(cbs.items()): + if 'multisig_funding_privkey' not in cb: + cb['multisig_funding_privkey'] = None + self.data['seed_version'] = 60 + def _convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return diff --git a/tests/regtest/regtest.sh b/tests/regtest/regtest.sh index bcb882f9b..3b2b5d454 100755 --- a/tests/regtest/regtest.sh +++ b/tests/regtest/regtest.sh @@ -156,6 +156,7 @@ if [[ $1 == "backup" ]]; then echo "alice opens channel" bob_node=$($bob nodeid) channel1=$($alice open_channel $bob_node 0.15 --password='') + new_blocks 1 # cannot open multiple chans with same node in same block $alice setconfig use_recoverable_channels False channel2=$($alice open_channel $bob_node 0.15 --password='') new_blocks 3 diff --git a/tests/test_lnutil.py b/tests/test_lnutil.py index 3d03b9b52..79097fff4 100644 --- a/tests/test_lnutil.py +++ b/tests/test_lnutil.py @@ -1086,6 +1086,7 @@ class TestLNUtil(ElectrumTestCase): remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), local_payment_pubkey=None, + multisig_funding_privkey=None, ), decoded_cb, ) @@ -1113,6 +1114,7 @@ class TestLNUtil(ElectrumTestCase): remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), local_payment_pubkey=bfh('0308d686712782a44b0cef220485ad83dae77853a5bf8501a92bb79056c9dcb25a'), + multisig_funding_privkey=None, ), decoded_cb, )