Skip to content

Commit

Permalink
utxo privacy analysis:
Browse files Browse the repository at this point in the history
 - show number of parents in tab
 - full list of parents in details dialog
  • Loading branch information
ecdsa committed Feb 11, 2023
1 parent df842af commit 3a3f267
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 11 deletions.
32 changes: 32 additions & 0 deletions electrum/address_synchronizer.py
Expand Up @@ -88,6 +88,7 @@ def __init__(self, db: 'WalletDB', config: 'SimpleConfig', *, name: str = None):
self.threadlocal_cache = threading.local()

self._get_balance_cache = {}
self._tx_parents_cache = {}

self.load_and_cleanup()

Expand Down Expand Up @@ -348,6 +349,7 @@ def add_value_from_prev_output():
# save
self.db.add_transaction(tx_hash, tx)
self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs()))
self._tx_parents_cache.clear()
if is_new:
util.trigger_callback('adb_added_tx', self, tx_hash)
return True
Expand Down Expand Up @@ -951,3 +953,33 @@ def address_is_old(self, address: str, *, req_conf: int = 3) -> bool:
tx_age = self.get_local_height() - tx_height + 1
max_conf = max(max_conf, tx_age)
return max_conf >= req_conf

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
"""
from .util import ShortID
if not self.is_up_to_date():
return {}

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.get_transaction(txid)
for i, txin in enumerate(tx.inputs()):
_txid = txin.prevout.txid.hex()
if _txid in self.db.transactions:
#parents.append(txin.prevout)
parents.append(str(txin.short_id))
result.update(self.get_tx_parents(_txid))
else:
# maybe add it too?
parents.append(None)
result[txid] = parents
self._tx_parents_cache[txid] = result
return result
5 changes: 5 additions & 0 deletions electrum/gui/qt/main_window.py
Expand Up @@ -1065,6 +1065,11 @@ def show_address(self, addr: str, *, parent: QWidget = None):
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()
Expand Down
105 changes: 105 additions & 0 deletions electrum/gui/qt/utxo_dialog.py
@@ -0,0 +1,105 @@
#!/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
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.adb.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)
vbox.addLayout(Buttons(CloseButton(self)))
self.setLayout(vbox)

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)
22 changes: 11 additions & 11 deletions electrum/gui/qt/utxo_list.py
Expand Up @@ -49,10 +49,12 @@ class Columns(IntEnum):
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'),
}
Expand Down Expand Up @@ -87,14 +89,15 @@ def update(self):
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)
Expand All @@ -117,8 +120,10 @@ def refresh_row(self, key, row):
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.adb.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:
Expand All @@ -130,7 +135,7 @@ def refresh_row(self, key, row):
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):
Expand Down Expand Up @@ -261,12 +266,7 @@ def create_menu(self, position):
if len(coins) == 1:
utxo = coins[0]
addr = utxo.address
txid = utxo.prevout.txid.hex()
# "Details"
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))
# "Copy ..."
idx = self.indexAt(position)
if not idx.isValid():
Expand Down

0 comments on commit 3a3f267

Please sign in to comment.