1
0

Merge pull request #9430 from SomberNight/202501_funding_pubkey_deriv

lightning: change derivation of funding_pubkey
This commit is contained in:
ThomasV
2025-01-17 10:49:16 +01:00
committed by GitHub
10 changed files with 190 additions and 24 deletions

View File

@@ -415,6 +415,10 @@ class AbstractChannel(Logger, ABC):
def get_funding_address(self) -> str: def get_funding_address(self) -> str:
pass pass
def get_funding_tx(self) -> Optional[Transaction]:
funding_txid = self.funding_outpoint.txid
return self.lnworker.lnwatcher.adb.get_transaction(funding_txid)
@abstractmethod @abstractmethod
def get_sweep_address(self) -> str: def get_sweep_address(self) -> str:
"""Returns a wallet address we can use to sweep coins to. """Returns a wallet address we can use to sweep coins to.
@@ -534,6 +538,12 @@ class ChannelBackup(AbstractChannel):
self.logger.warning( self.logger.warning(
f"local_payment_pubkey missing from (old-type) channel backup. " f"local_payment_pubkey missing from (old-type) channel backup. "
f"You should export and re-import a newer 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( self.config[LOCAL] = LocalConfig.from_seed(
channel_seed=cb.channel_seed, channel_seed=cb.channel_seed,
to_self_delay=cb.local_delay, 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 # 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 # 3. anchor outputs: sweep to_remote by deriving the key from the funding pubkeys
static_remotekey=local_payment_pubkey, static_remotekey=local_payment_pubkey,
multisig_key=multisig_funding_keypair,
# dummy values # dummy values
static_payment_key=None, static_payment_key=None,
dust_limit_sat=None, dust_limit_sat=None,
@@ -594,7 +605,9 @@ class ChannelBackup(AbstractChannel):
return True return True
def create_sweeptxs_for_their_ctx(self, ctx): 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): def create_sweeptxs_for_our_ctx(self, ctx):
if self.is_imported: if self.is_imported:

View File

@@ -46,7 +46,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, ChannelConf
IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage, IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage,
ChannelType, LNProtocolWarning, validate_features, IncompatibleOrInsaneFeatures) ChannelType, LNProtocolWarning, validate_features, IncompatibleOrInsaneFeatures)
from .lnutil import FeeUpdate, channel_id_from_funding_tx, PaymentFeeBudget 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 .lntransport import LNTransport, LNTransportBase, LightningPeerConnectionClosed, HandshakeFailed
from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg
from .interface import GracefulDisconnect from .interface import GracefulDisconnect
@@ -673,7 +673,15 @@ class Peer(Logger, EventListener):
self.logger.info(f"upfront shutdown script received: {upfront_shutdown_script}") self.logger.info(f"upfront shutdown script received: {upfront_shutdown_script}")
return 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) channel_seed = os.urandom(32)
initial_msat = funding_sat * 1000 - push_msat if initiator == LOCAL else push_msat 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_payment_key = None
static_remotekey = bytes.fromhex(wallet.get_public_key(addr)) 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 dust_limit_sat = bitcoin.DUST_LIMIT_P2PKH
reserve_sat = max(funding_sat // 100, dust_limit_sat) reserve_sat = max(funding_sat // 100, dust_limit_sat)
# for comparison of defaults, see # for comparison of defaults, see
@@ -702,6 +718,7 @@ class Peer(Logger, EventListener):
channel_seed=channel_seed, channel_seed=channel_seed,
static_remotekey=static_remotekey, static_remotekey=static_remotekey,
static_payment_key=static_payment_key, static_payment_key=static_payment_key,
multisig_key=multisig_funding_keypair,
upfront_shutdown_script=upfront_shutdown_script, upfront_shutdown_script=upfront_shutdown_script,
to_self_delay=self.network.config.LIGHTNING_TO_SELF_DELAY_CSV, to_self_delay=self.network.config.LIGHTNING_TO_SELF_DELAY_CSV,
dust_limit_sat=dust_limit_sat, dust_limit_sat=dust_limit_sat,
@@ -787,7 +804,21 @@ class Peer(Logger, EventListener):
'type': our_channel_type.to_bytes_minimal() '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. # if it includes open_channel_tlvs: MUST include upfront_shutdown_script.
open_channel_tlvs['upfront_shutdown_script'] = { open_channel_tlvs['upfront_shutdown_script'] = {
'shutdown_scriptpubkey': local_config.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): if not channel_type.complies_with_features(self.features):
raise Exception("sender has sent a channel type we don't support") 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( upfront_shutdown_script = self.upfront_shutdown_script_from_payload(
payload, 'open') payload, 'open')

View File

@@ -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, get_ordered_channel_configs, get_per_commitment_secret_from_seed,
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED, RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script, 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, from .transaction import (Transaction, TxInput, PartialTxInput,
PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template) PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template)
from .simple_config import SimpleConfig 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( def sweep_their_ctx_to_remote_backup(
*, chan: 'ChannelBackup', *, chan: 'ChannelBackup',
ctx: Transaction) -> Optional[Dict[str, SweepInfo]]: ctx: Transaction,
funding_tx: Transaction,
) -> Optional[Dict[str, SweepInfo]]:
txs = {} # type: Dict[str, SweepInfo] txs = {} # type: Dict[str, SweepInfo]
"""If we only have a backup, and the remote force-closed with their ctx, """If we only have a backup, and the remote force-closed with their ctx,
and anchors are enabled, we need to sweep to_remote.""" 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]) 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]}') _logger.debug(f'checking their ctx for funding pubkeys: {[pk.hex() for pk in funding_pubkeys]}')
# check which of the pubkey was ours # 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_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) 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): if ctx.get_output_idxs_from_address(candidate_to_remote_address):
@@ -508,8 +511,33 @@ def sweep_their_ctx_to_remote_backup(
return {} return {}
# remote anchor # 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 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( txs[txin.prevout.to_str()] = SweepInfo(
name='remote_anchor', name='remote_anchor',
csv_delay=0, csv_delay=0,
@@ -517,9 +545,6 @@ def sweep_their_ctx_to_remote_backup(
txin=txin, txin=txin,
txout=None, txout=None,
) )
else:
# fixme: onchain channel backups do not store the channel seed
pass
# to_remote # to_remote
csv_delay = 1 csv_delay = 1
@@ -810,7 +835,7 @@ def sweep_their_ctx_to_remote(
return txin 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 from .lnutil import make_commitment_output_to_anchor_address, make_commitment_output_to_anchor_witness_script
local_funding_pubkey = multisig_key.pubkey local_funding_pubkey = multisig_key.pubkey
local_anchor_address = make_commitment_output_to_anchor_address(local_funding_pubkey) local_anchor_address = make_commitment_output_to_anchor_address(local_funding_pubkey)

View File

@@ -224,7 +224,8 @@ class LocalConfig(ChannelConfig):
node = BIP32Node.from_rootseed(channel_seed, xtype='standard') node = BIP32Node.from_rootseed(channel_seed, xtype='standard')
keypair_generator = lambda family: generate_keypair(node, family) keypair_generator = lambda family: generate_keypair(node, family)
kwargs['per_commitment_secret_seed'] = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey 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['htlc_basepoint'] = keypair_generator(LnKeyFamily.HTLC_BASE)
kwargs['delayed_basepoint'] = keypair_generator(LnKeyFamily.DELAY_BASE) kwargs['delayed_basepoint'] = keypair_generator(LnKeyFamily.DELAY_BASE)
kwargs['revocation_basepoint'] = keypair_generator(LnKeyFamily.REVOCATION_BASE) kwargs['revocation_basepoint'] = keypair_generator(LnKeyFamily.REVOCATION_BASE)
@@ -279,8 +280,8 @@ class ChannelConstraints(StoredObject):
funding_txn_minimum_depth = attr.ib(type=int) funding_txn_minimum_depth = attr.ib(type=int)
CHANNEL_BACKUP_VERSION_LATEST = 1 CHANNEL_BACKUP_VERSION_LATEST = 2
KNOWN_CHANNEL_BACKUP_VERSIONS = (0, 1,) KNOWN_CHANNEL_BACKUP_VERSIONS = (0, 1, 2, )
assert CHANNEL_BACKUP_VERSION_LATEST in KNOWN_CHANNEL_BACKUP_VERSIONS assert CHANNEL_BACKUP_VERSION_LATEST in KNOWN_CHANNEL_BACKUP_VERSIONS
@attr.s @attr.s
@@ -315,6 +316,7 @@ class ImportedChannelBackupStorage(ChannelBackupStorage):
remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)
remote_revocation_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] 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: def to_bytes(self) -> bytes:
vds = BCDataStream() vds = BCDataStream()
@@ -333,6 +335,7 @@ class ImportedChannelBackupStorage(ChannelBackupStorage):
vds.write_string(self.host) vds.write_string(self.host)
vds.write_uint16(self.port) vds.write_uint16(self.port)
vds.write_bytes(self.local_payment_pubkey, 33) vds.write_bytes(self.local_payment_pubkey, 33)
vds.write_bytes(self.multisig_funding_privkey, 32)
return bytes(vds.input) return bytes(vds.input)
@staticmethod @staticmethod
@@ -359,6 +362,10 @@ class ImportedChannelBackupStorage(ChannelBackupStorage):
local_payment_pubkey = vds.read_bytes(33) local_payment_pubkey = vds.read_bytes(33)
else: else:
local_payment_pubkey = None local_payment_pubkey = None
if version >= 2:
multisig_funding_privkey = vds.read_bytes(32)
else:
multisig_funding_privkey = None
return ImportedChannelBackupStorage( return ImportedChannelBackupStorage(
is_initiator=is_initiator, is_initiator=is_initiator,
privkey=privkey, privkey=privkey,
@@ -374,6 +381,7 @@ class ImportedChannelBackupStorage(ChannelBackupStorage):
host=host, host=host,
port=port, port=port,
local_payment_pubkey=local_payment_pubkey, local_payment_pubkey=local_payment_pubkey,
multisig_funding_privkey=multisig_funding_privkey,
) )
@staticmethod @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( def make_htlc_tx_output(
amount_msat, amount_msat,
local_feerate, local_feerate,
@@ -1693,6 +1749,7 @@ class LnKeyFamily(IntEnum):
BACKUP_CIPHER = 7 | BIP32_PRIME BACKUP_CIPHER = 7 | BIP32_PRIME
PAYMENT_SECRET_KEY = 8 | BIP32_PRIME PAYMENT_SECRET_KEY = 8 | BIP32_PRIME
NOSTR_KEY = 9 | BIP32_PRIME NOSTR_KEY = 9 | BIP32_PRIME
FUNDING_ROOT_KEY = 10 | BIP32_PRIME
def generate_keypair(node: BIP32Node, key_family: LnKeyFamily) -> Keypair: def generate_keypair(node: BIP32Node, key_family: LnKeyFamily) -> Keypair:

View File

@@ -132,7 +132,7 @@ class LNWatcher(Logger, EventListener):
if not keep_watching: if not keep_watching:
await self.unwatch_channel(address, funding_outpoint) 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 raise NotImplementedError() # implemented by subclasses
async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str, async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str,

View File

@@ -103,6 +103,7 @@ NUM_PEERS_TARGET = 4
# onchain channel backup data # onchain channel backup data
CB_VERSION = 0 CB_VERSION = 0
CB_MAGIC_BYTES = bytes([0, 0, 0, CB_VERSION]) CB_MAGIC_BYTES = bytes([0, 0, 0, CB_VERSION])
NODE_ID_PREFIX_LEN = 16
FALLBACK_NODE_LIST_TESTNET = ( 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.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.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.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) Logger.__init__(self)
features = LNWALLET_FEATURES features = LNWALLET_FEATURES
if self.config.ENABLE_ANCHOR_CHANNELS: if self.config.ENABLE_ANCHOR_CHANNELS:
@@ -1368,8 +1370,8 @@ class LNWallet(LNWorker):
self.remove_channel(chan.channel_id) self.remove_channel(chan.channel_id)
raise raise
def cb_data(self, node_id): def cb_data(self, node_id: bytes) -> bytes:
return CB_MAGIC_BYTES + node_id[0:16] return CB_MAGIC_BYTES + node_id[0:NODE_ID_PREFIX_LEN]
def decrypt_cb_data(self, encrypted_data, funding_address): def decrypt_cb_data(self, encrypted_data, funding_address):
funding_scripthash = bytes.fromhex(address_to_scripthash(funding_address)) funding_scripthash = bytes.fromhex(address_to_scripthash(funding_address))
@@ -1389,6 +1391,8 @@ class LNWallet(LNWorker):
funding_sat: int, funding_sat: int,
node_id: bytes, node_id: bytes,
fee_est=None) -> PartialTransaction: fee_est=None) -> PartialTransaction:
from .wallet import get_locktime_for_new_transaction
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.CHANNEL, funding_sat)] outputs = [PartialTxOutput.from_address_and_value(DummyAddress.CHANNEL, funding_sat)]
if self.has_recoverable_channels(): if self.has_recoverable_channels():
dummy_scriptpubkey = make_op_return(self.cb_data(node_id)) dummy_scriptpubkey = make_op_return(self.cb_data(node_id))
@@ -1398,6 +1402,9 @@ class LNWallet(LNWorker):
outputs=outputs, outputs=outputs,
fee=fee_est) fee=fee_est)
tx.set_rbf(False) 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 return tx
def suggest_funding_amount(self, amount_to_pay, coins): 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_revocation_pubkey = chan.config[REMOTE].revocation_basepoint.pubkey,
remote_payment_pubkey = chan.config[REMOTE].payment_basepoint.pubkey, remote_payment_pubkey = chan.config[REMOTE].payment_basepoint.pubkey,
local_payment_pubkey=chan.config[LOCAL].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): def export_channel_backup(self, channel_id):
@@ -3114,7 +3122,7 @@ class LNWallet(LNWorker):
encrypted_data = o2.scriptpubkey[2:] encrypted_data = o2.scriptpubkey[2:]
data = self.decrypt_cb_data(encrypted_data, funding_address) data = self.decrypt_cb_data(encrypted_data, funding_address)
if data.startswith(CB_MAGIC_BYTES): 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: if node_id_prefix is None:
return return
funding_txid = tx.txid() funding_txid = tx.txid()

View File

@@ -206,7 +206,11 @@ async def sweep(
return tx 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 no network or not up to date, just set locktime to zero
if not network: if not network:
return 0 return 0
@@ -225,8 +229,9 @@ def get_locktime_for_new_transaction(network: 'Network') -> int:
locktime = min(chain_height, server_height) locktime = min(chain_height, server_height)
# sometimes pick locktime a bit further back, to help privacy # sometimes pick locktime a bit further back, to help privacy
# of setups that need more time (offline/multisig/coinjoin/...) # of setups that need more time (offline/multisig/coinjoin/...)
if random.randint(0, 9) == 0: if include_random_component:
locktime = max(0, locktime - random.randint(0, 99)) if random.randint(0, 9) == 0:
locktime = max(0, locktime - random.randint(0, 99))
locktime = max(0, locktime) locktime = max(0, locktime)
return locktime return locktime

View File

@@ -72,7 +72,7 @@ class WalletUnfinished(WalletFileException):
# seed_version is now used for the version of the wallet file # seed_version is now used for the version of the wallet file
OLD_SEED_VERSION = 4 # electrum versions < 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # 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 # old versions from overwriting new format
@@ -234,6 +234,7 @@ class WalletDBUpgrader(Logger):
self._convert_version_57() self._convert_version_57()
self._convert_version_58() self._convert_version_58()
self._convert_version_59() self._convert_version_59()
self._convert_version_60()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
def _convert_wallet_type(self): def _convert_wallet_type(self):
@@ -1146,6 +1147,15 @@ class WalletDBUpgrader(Logger):
self.data['channels'] = channels self.data['channels'] = channels
self.data['seed_version'] = 59 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): def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13): if not self._is_upgrade_method_needed(0, 13):
return return

View File

@@ -156,6 +156,7 @@ if [[ $1 == "backup" ]]; then
echo "alice opens channel" echo "alice opens channel"
bob_node=$($bob nodeid) bob_node=$($bob nodeid)
channel1=$($alice open_channel $bob_node 0.15 --password='') 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 $alice setconfig use_recoverable_channels False
channel2=$($alice open_channel $bob_node 0.15 --password='') channel2=$($alice open_channel $bob_node 0.15 --password='')
new_blocks 3 new_blocks 3

View File

@@ -1086,6 +1086,7 @@ class TestLNUtil(ElectrumTestCase):
remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'),
remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'),
local_payment_pubkey=None, local_payment_pubkey=None,
multisig_funding_privkey=None,
), ),
decoded_cb, decoded_cb,
) )
@@ -1113,6 +1114,7 @@ class TestLNUtil(ElectrumTestCase):
remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'),
remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'),
local_payment_pubkey=bfh('0308d686712782a44b0cef220485ad83dae77853a5bf8501a92bb79056c9dcb25a'), local_payment_pubkey=bfh('0308d686712782a44b0cef220485ad83dae77853a5bf8501a92bb79056c9dcb25a'),
multisig_funding_privkey=None,
), ),
decoded_cb, decoded_cb,
) )