Skip to content

Commit

Permalink
privacy analysis: detect address reuse
Browse files Browse the repository at this point in the history
add tx position to get_addr_io
  • Loading branch information
ecdsa committed Mar 4, 2023
1 parent 798cd60 commit 2ed7157
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 20 deletions.
6 changes: 3 additions & 3 deletions electrum/address_synchronizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,13 +778,13 @@ def get_addr_io(self, address):
sent = {}
for tx_hash, height in h:
hh, pos = self.get_txpos(tx_hash)
assert hh == height
d = self.db.get_txo_addr(tx_hash, address)
for n, (v, is_cb) in d.items():
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)
for txi, v in l:
sent[txi] = tx_hash, height
sent[txi] = tx_hash, height, pos
return received, sent

def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]:
Expand All @@ -799,7 +799,7 @@ def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]:
utxo.block_height = tx_height
utxo.block_txpos = tx_pos
if prevout_str in sent:
txid, height = sent[prevout_str]
txid, height, pos = sent[prevout_str]
utxo.spent_txid = txid
utxo.spent_height = height
else:
Expand Down
39 changes: 26 additions & 13 deletions electrum/gui/qt/utxo_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@

from PyQt5.QtCore import Qt, QUrl
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 .util import WindowModalDialog, ButtonsLineEdit, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT, WWLabel
from .history_list import HistoryList, HistoryModel
from .qrtextedit import ShowQRTextEdit
from .transaction_dialog import TxOutputColoring

if TYPE_CHECKING:
from .main_window import ElectrumWindow
Expand Down Expand Up @@ -66,6 +67,10 @@ def __init__(self, window: 'ElectrumWindow', utxo):
self.parents_list.setMinimumWidth(900)
self.parents_list.setMinimumHeight(400)
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()
ext = QTextCharFormat()
Expand All @@ -81,7 +86,7 @@ def __init__(self, window: 'ElectrumWindow', utxo):
ASCII_PIPE = '│'
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:
return
tx_height, tx_pos = self.wallet.adb.get_txpos(_txid)
Expand All @@ -91,7 +96,10 @@ def print_ascii_tree(_txid, prefix, is_last):
label = '[duplicate]'
c = '' if _txid == txid else (ASCII_EDGE if is_last else ASCII_BRANCH)
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.setAnchorHref(_txid)
#lnk.setAnchorNames([a_name])
Expand All @@ -102,22 +110,27 @@ def print_ascii_tree(_txid, prefix, is_last):
cursor.insertText(label, ext)
cursor.insertBlock()
next_prefix = '' if txid == _txid else prefix + (ASCII_SPACE if is_last else ASCII_PIPE)
parents_list = parents_copy.pop(_txid, [])
for i, p in enumerate(parents_list):
is_last = i == len(parents_list) - 1
print_ascii_tree(p, next_prefix, is_last)
parents_list, uncle_list = parents_copy.pop(_txid, ([],[]))
for i, p in enumerate(parents_list + uncle_list):
is_last = (i == len(parents_list) + len(uncle_list)- 1)
is_uncle = (i > len(parents_list) - 1)
print_ascii_tree(p, next_prefix, is_last, is_uncle)

# recursively build the tree
print_ascii_tree(txid, '', False)
print_ascii_tree(txid, '', False, False)
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(num_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))
legend_hbox = QHBoxLayout()
legend_hbox.setContentsMargins(0, 0, 0, 0)
legend_hbox.addStretch(2)
legend_hbox.addWidget(self.txo_color_parent.legend_label)
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)))
self.setLayout(vbox)
# set cursor to top
Expand Down
22 changes: 18 additions & 4 deletions electrum/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,23 +871,37 @@ def get_tx_parents(self, txid) -> Dict:
"""
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:
if self._last_full_history is None:
self._last_full_history = self.get_full_history(None)
result = self._tx_parents_cache.get(txid, None)
if result is not None:
return result
result = {}
parents = []
uncles = []
tx = self.adb.get_transaction(txid)
assert tx, f"cannot find {txid} in db"
for i, txin in enumerate(tx.inputs()):
_txid = txin.prevout.txid.hex()
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():
result.update(self.get_tx_parents(_txid))
result[txid] = parents
result[txid] = parents, uncles
self._tx_parents_cache[txid] = result
return result

Expand Down

0 comments on commit 2ed7157

Please sign in to comment.