Browse Source

wallet: implement reserving addresses, and use it for LN SRK to_remote

- Use change addresses (instead of receive) for the static_remotekey to_remote outputs,
  and reserve these to greatly reduce the chance of address-reuse
- Use change addresses (instead of receive) for LN channel sweep addresses.
  Note that these atm are not getting reserved.
master
SomberNight 5 years ago
parent
commit
6040e953a3
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 10
      electrum/lnchannel.py
  2. 2
      electrum/lnpeer.py
  3. 9
      electrum/lnworker.py
  4. 34
      electrum/wallet.py

10
electrum/lnchannel.py

@ -51,7 +51,7 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey
ScriptHtlc, PaymentFailure, calc_fees_for_commitment_tx, RemoteMisbehaving, make_htlc_output_witness_script,
ShortChannelID, map_htlcs_to_ctx_output_idxs, LNPeerAddr,
LN_MAX_HTLC_VALUE_MSAT, fee_for_htlc_output, offered_htlc_trim_threshold_sat,
received_htlc_trim_threshold_sat)
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address)
from .lnsweep import create_sweeptxs_for_our_ctx, create_sweeptxs_for_their_ctx
from .lnsweep import create_sweeptx_for_their_revoked_htlc, SweepInfo
from .lnhtlc import HTLCManager
@ -601,6 +601,14 @@ class Channel(AbstractChannel):
def is_static_remotekey_enabled(self) -> bool:
return bool(self.storage.get('static_remotekey_enabled'))
def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:
ret = []
if 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)
ret.append(to_remote_address)
return ret
def get_feerate(self, subject: HTLCOwner, *, ctn: int) -> int:
# returns feerate in sat/kw
return self.hm.get_feerate(subject, ctn)

2
electrum/lnpeer.py

@ -496,7 +496,7 @@ class Peer(Logger):
# we will want to derive that key
wallet = self.lnworker.wallet
assert wallet.txin_type == 'p2wpkh'
addr = wallet.get_unused_address()
addr = wallet.get_new_sweep_address_for_channel()
static_remotekey = bfh(wallet.get_public_key(addr))
else:
static_remotekey = None

9
electrum/lnworker.py

@ -488,7 +488,7 @@ class LNWallet(LNWorker):
self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid
self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
self.sweep_address = wallet.get_receiving_address()
self.sweep_address = wallet.get_new_sweep_address_for_channel() # TODO possible address-reuse
self.logs = defaultdict(list) # type: Dict[str, List[PaymentAttemptLog]] # key is RHASH # (not persisted)
self.is_routing = set() # (not persisted) keys of invoices that are in PR_ROUTING state
# used in tests
@ -770,6 +770,8 @@ class LNWallet(LNWorker):
self.add_channel(chan)
channels_db = self.db.get_dict('channels')
channels_db[chan.channel_id.hex()] = chan.storage
for addr in chan.get_wallet_addresses_channel_might_want_reserved():
self.wallet.set_reserved_state_of_address(addr, reserved=True)
self.wallet.save_backup()
def mktx_for_open_channel(self, *, coins: Sequence[PartialTxInput], funding_sat: int,
@ -1309,6 +1311,8 @@ class LNWallet(LNWorker):
with self.lock:
self._channels.pop(chan_id)
self.db.get('channels').pop(chan_id.hex())
for addr in chan.get_wallet_addresses_channel_might_want_reserved():
self.wallet.set_reserved_state_of_address(addr, reserved=False)
util.trigger_callback('channels_updated', self.wallet)
util.trigger_callback('wallet_updated', self.wallet)
@ -1404,7 +1408,8 @@ class LNBackups(Logger):
@property
def sweep_address(self) -> str:
return self.wallet.get_receiving_address()
# TODO possible address-reuse
return self.wallet.get_new_sweep_address_for_channel()
def channel_state_changed(self, chan):
util.trigger_callback('channel', chan)

34
electrum/wallet.py

@ -247,6 +247,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.fiat_value = db.get_dict('fiat_value')
self.receive_requests = db.get_dict('payment_requests')
self.invoices = db.get_dict('invoices')
self._reserved_addresses = set(db.get('reserved_addresses', []))
self._prepare_onchain_invoice_paid_detection()
self.calc_unused_change_addresses()
@ -386,7 +387,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
addrs = self._unused_change_addresses
else:
addrs = self.get_change_addresses()
self._unused_change_addresses = [addr for addr in addrs if not self.is_used(addr)]
self._unused_change_addresses = [addr for addr in addrs
if not self.is_used(addr) and not self.is_address_reserved(addr)]
return list(self._unused_change_addresses)
def is_deterministic(self) -> bool:
@ -1046,6 +1048,22 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
max_change = self.max_change_outputs if self.multiple_change else 1
return change_addrs[:max_change]
@check_returned_address_for_corruption
def get_new_sweep_address_for_channel(self) -> str:
# Recalc and get unused change addresses
addrs = self.calc_unused_change_addresses()
if addrs:
selected_addr = addrs[0]
else:
# if there are none, take one randomly from the last few
addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
if addrs:
selected_addr = random.choice(addrs)
else: # fallback for e.g. imported wallets
selected_addr = self.get_receiving_address()
assert is_address(selected_addr), f"not valid bitcoin address: {selected_addr}"
return selected_addr
def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
outputs: List[PartialTxOutput], fee=None,
change_addr: str = None, is_sweep=False) -> PartialTransaction:
@ -1182,6 +1200,20 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.frozen_coins -= set(utxos)
self.db.put('frozen_coins', list(self.frozen_coins))
def is_address_reserved(self, addr: str) -> bool:
# note: atm 'reserved' status is only taken into consideration for 'change addresses'
return addr in self._reserved_addresses
def set_reserved_state_of_address(self, addr: str, *, reserved: bool) -> None:
if not self.is_mine(addr):
return
with self.lock:
if reserved:
self._reserved_addresses.add(addr)
else:
self._reserved_addresses.discard(addr)
self.db.put('reserved_addresses', list(self._reserved_addresses))
def can_export(self):
return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')

Loading…
Cancel
Save