Merge pull request #9430 from SomberNight/202501_funding_pubkey_deriv
lightning: change derivation of funding_pubkey
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user