1
0

lightning: change derivation of funding_pubkey

Ideally, given an on-chain backup, after the remote force-closes, we should be able to spend our anchor output,
to CPFP the remote commitment tx (assuming the channel used OPTION_ANCHORS).
To spend the anchor output, we need to be able to sign with the local funding_privkey.

Previously we derived the funding_key from the channel_seed (which comes from os.urandom).
Prior to anchors, there was no use case for signing with the funding_key given a channel backup.
Now with anchors, we should make its derivation deterministic somehow, in a way so that it can
be derived given just an on-chain backup.
- one way would be to put some more data into the existing OP_RETURN
  - uses block space
  - the OP_RETURNs can be disabled via "use_recoverable_channels"
  - only the initiator can use OP_RETURNs (so what if channel is in incoming dir?)
- instead, new scheme for our funding_key:
  - we derive the funding_privkey from the lnworker root secret (derived from our bip32 seed)
  - for outgoing channels:
    - lnworker_root_secret + remote_node_id + funding_tx_nlocktime
  - for incoming channels:
    - lnworker_root_secret + remote_node_id + remote_funding_pubkey
  - a check is added to avoid reusing the same key between channels:
      not letting to user open more than one channel with the same peer in a single block
  - only the first 16 bytes of the remote_node_id are used, as the onchain backup OP_RETURNs only contain that
- as the funding_privkey cannot be derived from the channel_seed anymore, it is included in the
imported channel backups, which in turn need a new version defined
  - a wallet db upgrade is used to update already stored imported cbs
  - alternatively we could keep the imported cbs as-is, so no new version, no new funding_privkey field, as it is clearly somewhat redundant given on-chain backups can reconstruct it
    - however adding the field seems easier
      - otherwise the existing code would try to derive the funding_privkey from the channel_seed
      - also note: atm there is no field in the imported backups to distinguish anchor channels vs static-remotekey channels
This commit is contained in:
SomberNight
2025-01-14 16:14:01 +00:00
parent 8f5b395ddc
commit cba073dfd1
10 changed files with 190 additions and 24 deletions

View File

@@ -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: