commit 6040e953a32803a65a10f62d4f7ac815d1a9e590
parent 6457bb141dc2e4848c932c7478ab21b4424ba502
Author: SomberNight <somber.night@protonmail.com>
Date: Fri, 22 May 2020 15:34:30 +0200
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.
Diffstat:
4 files changed, 50 insertions(+), 5 deletions(-)
diff --git a/electrum/lnchannel.py b/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)
diff --git a/electrum/lnpeer.py b/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
diff --git a/electrum/lnworker.py b/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)
diff --git a/electrum/wallet.py b/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')