Browse Source

Merge pull request #8204 from spesmilo/privacy_analysis

Privacy analysis
ThomasV 3 years ago committed by GitHub
parent
commit
e72b76707a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      electrum/address_synchronizer.py
  2. 5
      electrum/gui/qt/main_window.py
  3. 113
      electrum/gui/qt/utxo_dialog.py
  4. 17
      electrum/gui/qt/utxo_list.py
  5. 57
      electrum/wallet.py

3
electrum/address_synchronizer.py

@ -349,7 +349,7 @@ class AddressSynchronizer(Logger, EventListener):
self.db.add_transaction(tx_hash, tx)
self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs()))
if is_new:
util.trigger_callback('adb_added_tx', self, tx_hash)
util.trigger_callback('adb_added_tx', self, tx_hash, tx)
return True
def remove_transaction(self, tx_hash: str) -> None:
@ -401,6 +401,7 @@ class AddressSynchronizer(Logger, EventListener):
scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey.hex())
prevout = TxOutpoint(bfh(tx_hash), idx)
self.db.remove_prevout_by_scripthash(scripthash, prevout=prevout, value=txo.value)
util.trigger_callback('adb_removed_tx', self, tx_hash, tx)
def get_depending_transactions(self, tx_hash: str) -> Set[str]:
"""Returns all (grand-)children of tx_hash in this wallet."""

5
electrum/gui/qt/main_window.py

@ -1065,6 +1065,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
d = address_dialog.AddressDialog(self, addr, parent=parent)
d.exec_()
def show_utxo(self, utxo):
from . import utxo_dialog
d = utxo_dialog.UTXODialog(self, utxo)
d.exec_()
def show_channel_details(self, chan):
from .channel_details import ChannelDetailsDialog
ChannelDetailsDialog(self, chan).show()

113
electrum/gui/qt/utxo_dialog.py

@ -0,0 +1,113 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2023 The Electrum Developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QTextCharFormat, QFont
from PyQt5.QtWidgets import QVBoxLayout, QLabel, QTextBrowser
from electrum.i18n import _
from .util import WindowModalDialog, ButtonsLineEdit, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT, WWLabel
from .history_list import HistoryList, HistoryModel
from .qrtextedit import ShowQRTextEdit
if TYPE_CHECKING:
from .main_window import ElectrumWindow
# todo:
# - edit label in tx detail window
class UTXODialog(WindowModalDialog):
def __init__(self, window: 'ElectrumWindow', utxo):
WindowModalDialog.__init__(self, window, _("Coin Privacy Analysis"))
self.main_window = window
self.config = window.config
self.wallet = window.wallet
self.utxo = utxo
txid = self.utxo.prevout.txid.hex()
parents = self.wallet.get_tx_parents(txid)
out = []
for _txid, _list in sorted(parents.items()):
tx_height, tx_pos = self.wallet.adb.get_txpos(_txid)
label = self.wallet.get_label_for_txid(_txid) or "<no label>"
out.append((tx_height, tx_pos, _txid, label, _list))
self.parents_list = QTextBrowser()
self.parents_list.setOpenLinks(False) # disable automatic link opening
self.parents_list.anchorClicked.connect(self.open_tx) # send links to our handler
self.parents_list.setFont(QFont(MONOSPACE_FONT))
self.parents_list.setReadOnly(True)
self.parents_list.setTextInteractionFlags(self.parents_list.textInteractionFlags() | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
self.parents_list.setMinimumWidth(900)
self.parents_list.setMinimumHeight(400)
self.parents_list.setLineWrapMode(QTextBrowser.NoWrap)
cursor = self.parents_list.textCursor()
ext = QTextCharFormat()
for tx_height, tx_pos, _txid, label, _list in reversed(sorted(out)):
key = "%dx%d"%(tx_height, tx_pos) if tx_pos >= 0 else _txid[0:8]
list_str = ','.join(filter(None, _list))
lnk = QTextCharFormat()
lnk.setToolTip(_('Click to open, right-click for menu'))
lnk.setAnchorHref(_txid)
#lnk.setAnchorNames([a_name])
lnk.setAnchor(True)
lnk.setUnderlineStyle(QTextCharFormat.SingleUnderline)
cursor.insertText(key, lnk)
cursor.insertText("\t", ext)
cursor.insertText("%-32s\t<- "%label[0:32], ext)
cursor.insertText(list_str, ext)
cursor.insertBlock()
vbox = QVBoxLayout()
vbox.addWidget(QLabel(_("Output point") + ": " + str(self.utxo.short_id)))
vbox.addWidget(QLabel(_("Amount") + ": " + self.main_window.format_amount_and_units(self.utxo.value_sats())))
vbox.addWidget(QLabel(_("This UTXO has {} parent transactions in your wallet").format(len(parents))))
vbox.addWidget(self.parents_list)
msg = ' '.join([
_("Note: This analysis only shows parent transactions, and does not take address reuse into consideration."),
_("If you reuse addresses, more links can be established between your transactions, that are not displayed here.")
])
vbox.addWidget(WWLabel(msg))
vbox.addLayout(Buttons(CloseButton(self)))
self.setLayout(vbox)
# set cursor to top
cursor.setPosition(0)
self.parents_list.setTextCursor(cursor)
def open_tx(self, txid):
if isinstance(txid, QUrl):
txid = txid.toString(QUrl.None_)
tx = self.wallet.adb.get_transaction(txid)
if not tx:
return
label = self.wallet.get_label_for_txid(txid)
self.main_window.show_transaction(tx, tx_desc=label)

17
electrum/gui/qt/utxo_list.py

@ -49,10 +49,12 @@ class UTXOList(MyTreeView):
ADDRESS = 1
LABEL = 2
AMOUNT = 3
PARENTS = 4
headers = {
Columns.OUTPOINT: _('Output point'),
Columns.ADDRESS: _('Address'),
Columns.PARENTS: _('Parents'),
Columns.LABEL: _('Label'),
Columns.AMOUNT: _('Amount'),
}
@ -87,14 +89,15 @@ class UTXOList(MyTreeView):
name = utxo.prevout.to_str()
self._utxo_dict[name] = utxo
address = utxo.address
amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True)
labels = [str(utxo.short_id), address, '', amount]
amount_str = self.parent.format_amount(utxo.value_sats(), whitespaces=True)
labels = [str(utxo.short_id), address, '', amount_str, '']
utxo_item = [QStandardItem(x) for x in labels]
self.set_editability(utxo_item)
utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_CLIPBOARD_DATA)
utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_PREVOUT_STR)
utxo_item[self.Columns.ADDRESS].setFont(QFont(MONOSPACE_FONT))
utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT))
utxo_item[self.Columns.PARENTS].setFont(QFont(MONOSPACE_FONT))
utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT))
self.model().insertRow(idx, utxo_item)
self.refresh_row(name, idx)
@ -117,8 +120,10 @@ class UTXOList(MyTreeView):
assert row is not None
utxo = self._utxo_dict[key]
utxo_item = [self.std_model.item(row, col) for col in self.Columns]
address = utxo.address
label = self.wallet.get_label_for_txid(utxo.prevout.txid.hex()) or self.wallet.get_label_for_address(address)
txid = utxo.prevout.txid.hex()
parents = self.wallet.get_tx_parents(txid)
utxo_item[self.Columns.PARENTS].setText('%6s'%len(parents))
label = self.wallet.get_label_for_txid(txid) or ''
utxo_item[self.Columns.LABEL].setText(label)
SELECTED_TO_SPEND_TOOLTIP = _('Coin selected to be spent')
if key in self._spend_set:
@ -130,7 +135,7 @@ class UTXOList(MyTreeView):
for col in utxo_item:
col.setBackground(color)
col.setToolTip(tooltip)
if self.wallet.is_frozen_address(address):
if self.wallet.is_frozen_address(utxo.address):
utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True))
utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen'))
if self.wallet.is_frozen_coin(utxo):
@ -257,7 +262,7 @@ class UTXOList(MyTreeView):
tx = self.wallet.adb.get_transaction(txid)
if tx:
label = self.wallet.get_label_for_txid(txid)
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, tx_desc=label))
menu.addAction(_("View parents"), lambda: self.parent.show_utxo(utxo))
# fully spend
menu_spend = menu.addMenu(_("Fully spend") + '')
m = menu_spend.addAction(_("send to address in clipboard"), lambda: self.pay_to_clipboard_address(coins))

57
electrum/wallet.py

@ -317,6 +317,8 @@ class Abstract_Wallet(ABC, Logger, EventListener):
self.adb.add_address(addr)
self.lock = self.adb.lock
self.transaction_lock = self.adb.transaction_lock
self._last_full_history = None
self._tx_parents_cache = {}
self.taskgroup = OldTaskGroup()
@ -453,6 +455,16 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def is_up_to_date(self) -> bool:
return self._up_to_date
def tx_is_related(self, tx):
is_mine = any([self.is_mine(out.address) for out in tx.outputs()])
is_mine |= any([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()])
return is_mine
def clear_tx_parents_cache(self):
with self.lock, self.transaction_lock:
self._tx_parents_cache.clear()
self._last_full_history = None
@event_listener
async def on_event_adb_set_up_to_date(self, adb):
if self.adb != adb:
@ -473,21 +485,25 @@ class Abstract_Wallet(ABC, Logger, EventListener):
self.logger.info(f'set_up_to_date: {up_to_date}')
@event_listener
def on_event_adb_added_tx(self, adb, tx_hash):
def on_event_adb_added_tx(self, adb, tx_hash: str, tx: Transaction):
if self.adb != adb:
return
tx = self.db.get_transaction(tx_hash)
if not tx:
raise Exception(tx_hash)
is_mine = any([self.is_mine(out.address) for out in tx.outputs()])
is_mine |= any([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()])
if not is_mine:
if not self.tx_is_related(tx):
return
self.clear_tx_parents_cache()
if self.lnworker:
self.lnworker.maybe_add_backup_from_tx(tx)
self._update_invoices_and_reqs_touched_by_tx(tx_hash)
util.trigger_callback('new_transaction', self, tx)
@event_listener
def on_event_adb_removed_tx(self, adb, txid: str, tx: Transaction):
if self.adb != adb:
return
if not self.tx_is_related(tx):
return
self.clear_tx_parents_cache()
@event_listener
def on_event_adb_added_verified_tx(self, adb, tx_hash):
if adb != self.adb:
@ -845,6 +861,33 @@ class Abstract_Wallet(ABC, Logger, EventListener):
is_lightning_funding_tx=is_lightning_funding_tx,
)
def get_tx_parents(self, txid) -> Dict:
"""
recursively calls itself and returns a flat dict:
txid -> input_index -> prevout
note: this does not take into account address reuse
"""
if not self.is_up_to_date():
return {}
if self._last_full_history is None:
self._last_full_history = self.get_full_history(None)
with self.lock, self.transaction_lock:
result = self._tx_parents_cache.get(txid, None)
if result is not None:
return result
result = {}
parents = []
tx = self.adb.get_transaction(txid)
for i, txin in enumerate(tx.inputs()):
parents.append(str(txin.short_id))
_txid = txin.prevout.txid.hex()
if _txid in self._last_full_history.keys():
result.update(self.get_tx_parents(_txid))
result[txid] = parents
self._tx_parents_cache[txid] = result
return result
def get_balance(self, **kwargs):
domain = self.get_addresses()
return self.adb.get_balance(domain, **kwargs)

Loading…
Cancel
Save