Browse Source

privacy analysis: detect address reuse

add tx position to get_addr_io
master
ThomasV 3 years ago
parent
commit
2ed71579c3
  1. 6
      electrum/address_synchronizer.py
  2. 39
      electrum/gui/qt/utxo_dialog.py
  3. 22
      electrum/wallet.py

6
electrum/address_synchronizer.py

@ -778,13 +778,13 @@ class AddressSynchronizer(Logger, EventListener):
sent = {} sent = {}
for tx_hash, height in h: for tx_hash, height in h:
hh, pos = self.get_txpos(tx_hash) hh, pos = self.get_txpos(tx_hash)
assert hh == height
d = self.db.get_txo_addr(tx_hash, address) d = self.db.get_txo_addr(tx_hash, address)
for n, (v, is_cb) in d.items(): for n, (v, is_cb) in d.items():
received[tx_hash + ':%d'%n] = (height, pos, v, is_cb) received[tx_hash + ':%d'%n] = (height, pos, v, is_cb)
for tx_hash, height in h:
l = self.db.get_txi_addr(tx_hash, address) l = self.db.get_txi_addr(tx_hash, address)
for txi, v in l: for txi, v in l:
sent[txi] = tx_hash, height sent[txi] = tx_hash, height, pos
return received, sent return received, sent
def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]:
@ -799,7 +799,7 @@ class AddressSynchronizer(Logger, EventListener):
utxo.block_height = tx_height utxo.block_height = tx_height
utxo.block_txpos = tx_pos utxo.block_txpos = tx_pos
if prevout_str in sent: if prevout_str in sent:
txid, height = sent[prevout_str] txid, height, pos = sent[prevout_str]
utxo.spent_txid = txid utxo.spent_txid = txid
utxo.spent_height = height utxo.spent_height = height
else: else:

39
electrum/gui/qt/utxo_dialog.py

@ -28,13 +28,14 @@ import copy
from PyQt5.QtCore import Qt, QUrl from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QTextCharFormat, QFont from PyQt5.QtGui import QTextCharFormat, QFont
from PyQt5.QtWidgets import QVBoxLayout, QLabel, QTextBrowser from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QTextBrowser
from electrum.i18n import _ from electrum.i18n import _
from .util import WindowModalDialog, ButtonsLineEdit, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT, WWLabel from .util import WindowModalDialog, ButtonsLineEdit, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT, WWLabel
from .history_list import HistoryList, HistoryModel from .history_list import HistoryList, HistoryModel
from .qrtextedit import ShowQRTextEdit from .qrtextedit import ShowQRTextEdit
from .transaction_dialog import TxOutputColoring
if TYPE_CHECKING: if TYPE_CHECKING:
from .main_window import ElectrumWindow from .main_window import ElectrumWindow
@ -66,6 +67,10 @@ class UTXODialog(WindowModalDialog):
self.parents_list.setMinimumWidth(900) self.parents_list.setMinimumWidth(900)
self.parents_list.setMinimumHeight(400) self.parents_list.setMinimumHeight(400)
self.parents_list.setLineWrapMode(QTextBrowser.NoWrap) self.parents_list.setLineWrapMode(QTextBrowser.NoWrap)
self.txo_color_parent = TxOutputColoring(
legend=_("Direct parent"), color=ColorScheme.BLUE, tooltip=_("Direct parent"))
self.txo_color_uncle = TxOutputColoring(
legend=_("Address reuse"), color=ColorScheme.RED, tooltip=_("Address reuse"))
cursor = self.parents_list.textCursor() cursor = self.parents_list.textCursor()
ext = QTextCharFormat() ext = QTextCharFormat()
@ -81,7 +86,7 @@ class UTXODialog(WindowModalDialog):
ASCII_PIPE = '' ASCII_PIPE = ''
ASCII_SPACE = ' ' ASCII_SPACE = ' '
def print_ascii_tree(_txid, prefix, is_last): def print_ascii_tree(_txid, prefix, is_last, is_uncle):
if _txid not in parents: if _txid not in parents:
return return
tx_height, tx_pos = self.wallet.adb.get_txpos(_txid) tx_height, tx_pos = self.wallet.adb.get_txpos(_txid)
@ -91,7 +96,10 @@ class UTXODialog(WindowModalDialog):
label = '[duplicate]' label = '[duplicate]'
c = '' if _txid == txid else (ASCII_EDGE if is_last else ASCII_BRANCH) c = '' if _txid == txid else (ASCII_EDGE if is_last else ASCII_BRANCH)
cursor.insertText(prefix + c, ext) cursor.insertText(prefix + c, ext)
lnk = QTextCharFormat() if is_uncle:
lnk = QTextCharFormat(self.txo_color_uncle.text_char_format)
else:
lnk = QTextCharFormat(self.txo_color_parent.text_char_format)
lnk.setToolTip(_('Click to open, right-click for menu')) lnk.setToolTip(_('Click to open, right-click for menu'))
lnk.setAnchorHref(_txid) lnk.setAnchorHref(_txid)
#lnk.setAnchorNames([a_name]) #lnk.setAnchorNames([a_name])
@ -102,22 +110,27 @@ class UTXODialog(WindowModalDialog):
cursor.insertText(label, ext) cursor.insertText(label, ext)
cursor.insertBlock() cursor.insertBlock()
next_prefix = '' if txid == _txid else prefix + (ASCII_SPACE if is_last else ASCII_PIPE) next_prefix = '' if txid == _txid else prefix + (ASCII_SPACE if is_last else ASCII_PIPE)
parents_list = parents_copy.pop(_txid, []) parents_list, uncle_list = parents_copy.pop(_txid, ([],[]))
for i, p in enumerate(parents_list): for i, p in enumerate(parents_list + uncle_list):
is_last = i == len(parents_list) - 1 is_last = (i == len(parents_list) + len(uncle_list)- 1)
print_ascii_tree(p, next_prefix, is_last) is_uncle = (i > len(parents_list) - 1)
print_ascii_tree(p, next_prefix, is_last, is_uncle)
# recursively build the tree # recursively build the tree
print_ascii_tree(txid, '', False) print_ascii_tree(txid, '', False, False)
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.addWidget(QLabel(_("Output point") + ": " + str(self.utxo.short_id))) 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(_("Amount") + ": " + self.main_window.format_amount_and_units(self.utxo.value_sats())))
vbox.addWidget(QLabel(_("This UTXO has {} parent transactions in your wallet").format(num_parents))) vbox.addWidget(QLabel(_("This UTXO has {} parent transactions in your wallet").format(num_parents)))
vbox.addWidget(self.parents_list) vbox.addWidget(self.parents_list)
msg = ' '.join([ legend_hbox = QHBoxLayout()
_("Note: This analysis only shows parent transactions, and does not take address reuse into consideration."), legend_hbox.setContentsMargins(0, 0, 0, 0)
_("If you reuse addresses, more links can be established between your transactions, that are not displayed here.") legend_hbox.addStretch(2)
]) legend_hbox.addWidget(self.txo_color_parent.legend_label)
vbox.addWidget(WWLabel(msg)) legend_hbox.addWidget(self.txo_color_uncle.legend_label)
vbox.addLayout(legend_hbox)
self.txo_color_parent.legend_label.setVisible(True)
self.txo_color_uncle.legend_label.setVisible(True)
vbox.addLayout(Buttons(CloseButton(self))) vbox.addLayout(Buttons(CloseButton(self)))
self.setLayout(vbox) self.setLayout(vbox)
# set cursor to top # set cursor to top

22
electrum/wallet.py

@ -871,23 +871,37 @@ class Abstract_Wallet(ABC, Logger, EventListener):
""" """
if not self.is_up_to_date(): if not self.is_up_to_date():
return {} return {}
if self._last_full_history is None:
self._last_full_history = self.get_full_history(None)
with self.lock, self.transaction_lock: with self.lock, self.transaction_lock:
if self._last_full_history is None:
self._last_full_history = self.get_full_history(None)
result = self._tx_parents_cache.get(txid, None) result = self._tx_parents_cache.get(txid, None)
if result is not None: if result is not None:
return result return result
result = {} result = {}
parents = [] parents = []
uncles = []
tx = self.adb.get_transaction(txid) tx = self.adb.get_transaction(txid)
assert tx, f"cannot find {txid} in db" assert tx, f"cannot find {txid} in db"
for i, txin in enumerate(tx.inputs()): for i, txin in enumerate(tx.inputs()):
_txid = txin.prevout.txid.hex() _txid = txin.prevout.txid.hex()
parents.append(_txid) parents.append(_txid)
# detect address reuse
addr = self.adb.get_txin_address(txin)
received, sent = self.adb.get_addr_io(addr)
if len(sent) > 1:
my_txid, my_height, my_pos = sent[txin.prevout.to_str()]
assert my_txid == txid
for k, v in sent.items():
if k != txin.prevout.to_str():
reuse_txid, reuse_height, reuse_pos = v
if (reuse_height, reuse_pos) < (my_height, my_pos):
uncle_txid, uncle_index = k.split(':')
uncles.append(uncle_txid)
for _txid in parents + uncles:
if _txid in self._last_full_history.keys(): if _txid in self._last_full_history.keys():
result.update(self.get_tx_parents(_txid)) result.update(self.get_tx_parents(_txid))
result[txid] = parents result[txid] = parents, uncles
self._tx_parents_cache[txid] = result self._tx_parents_cache[txid] = result
return result return result

Loading…
Cancel
Save