electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 32f5feff8f9c3eaabdffd77825c4a31ab79e71a9
parent 861640949e479689aeb9d30121ba1f9ddb40ae71
Author: ghost43 <somber.night@protonmail.com>
Date:   Tue, 31 Jul 2018 19:37:46 +0200

Merge pull request #4538 from SomberNight/verifier_reorgs_st18

verifier: better handle reorgs (and storage upgrade)
Diffstat:
Melectrum/address_synchronizer.py | 92++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Melectrum/gui/kivy/uix/screens.py | 7+++----
Melectrum/gui/qt/history_list.py | 12+++++++-----
Melectrum/gui/stdio.py | 6+++---
Melectrum/gui/text.py | 6+++---
Melectrum/storage.py | 12+++++++++++-
Melectrum/util.py | 11+++++++++++
Melectrum/verifier.py | 7+++++--
Melectrum/wallet.py | 31++++++++++++++++++-------------
9 files changed, 115 insertions(+), 69 deletions(-)

diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py @@ -27,10 +27,11 @@ from collections import defaultdict from . import bitcoin from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY -from .util import PrintError, profiler, bfh +from .util import PrintError, profiler, bfh, VerifiedTxInfo, TxMinedStatus from .transaction import Transaction from .synchronizer import Synchronizer from .verifier import SPV +from .blockchain import hash_header from .i18n import _ TX_HEIGHT_LOCAL = -2 @@ -45,6 +46,7 @@ class UnrelatedTransactionException(AddTransactionException): def __str__(self): return _("Transaction is unrelated to this wallet.") + class AddressSynchronizer(PrintError): """ inherited by wallet @@ -61,8 +63,11 @@ class AddressSynchronizer(PrintError): self.transaction_lock = threading.RLock() # address -> list(txid, height) self.history = storage.get('addr_history',{}) - # Verified transactions. txid -> (height, timestamp, block_pos). Access with self.lock. - self.verified_tx = storage.get('verified_tx3', {}) + # Verified transactions. txid -> VerifiedTxInfo. Access with self.lock. + verified_tx = storage.get('verified_tx3', {}) + self.verified_tx = {} + for txid, (height, timestamp, txpos, header_hash) in verified_tx.items(): + self.verified_tx[txid] = VerifiedTxInfo(height, timestamp, txpos, header_hash) # Transactions pending verification. txid -> tx_height. Access with self.lock. self.unverified_tx = defaultdict(int) # true when synchronized @@ -90,7 +95,7 @@ class AddressSynchronizer(PrintError): with self.lock, self.transaction_lock: related_txns = self._history_local.get(addr, set()) for tx_hash in related_txns: - tx_height = self.get_tx_height(tx_hash)[0] + tx_height = self.get_tx_height(tx_hash).height h.append((tx_hash, tx_height)) return h @@ -193,7 +198,7 @@ class AddressSynchronizer(PrintError): # of add_transaction tx, we might learn of more-and-more inputs of # being is_mine, as we roll the gap_limit forward is_coinbase = tx.inputs()[0]['type'] == 'coinbase' - tx_height = self.get_tx_height(tx_hash)[0] + tx_height = self.get_tx_height(tx_hash).height if not allow_unrelated: # note that during sync, if the transactions are not properly sorted, # it could happen that we think tx is unrelated but actually one of the inputs is is_mine. @@ -212,10 +217,10 @@ class AddressSynchronizer(PrintError): conflicting_txns = self.get_conflicting_transactions(tx) if conflicting_txns: existing_mempool_txn = any( - self.get_tx_height(tx_hash2)[0] in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) + self.get_tx_height(tx_hash2).height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) for tx_hash2 in conflicting_txns) existing_confirmed_txn = any( - self.get_tx_height(tx_hash2)[0] > 0 + self.get_tx_height(tx_hash2).height > 0 for tx_hash2 in conflicting_txns) if existing_confirmed_txn and tx_height <= 0: # this is a non-confirmed tx that conflicts with confirmed txns; drop. @@ -393,7 +398,7 @@ class AddressSynchronizer(PrintError): def remove_local_transactions_we_dont_have(self): txid_set = set(self.txi) | set(self.txo) for txid in txid_set: - tx_height = self.get_tx_height(txid)[0] + tx_height = self.get_tx_height(txid).height if tx_height == TX_HEIGHT_LOCAL and txid not in self.transactions: self.remove_transaction(txid) @@ -431,11 +436,11 @@ class AddressSynchronizer(PrintError): self.save_transactions() def get_txpos(self, tx_hash): - "return position, even if the tx is unverified" + """Returns (height, txpos) tuple, even if the tx is unverified.""" with self.lock: if tx_hash in self.verified_tx: - height, timestamp, pos = self.verified_tx[tx_hash] - return height, pos + info = self.verified_tx[tx_hash] + return info.height, info.txpos elif tx_hash in self.unverified_tx: height = self.unverified_tx[tx_hash] return (height, 0) if height > 0 else ((1e9 - height), 0) @@ -462,16 +467,16 @@ class AddressSynchronizer(PrintError): history = [] for tx_hash in tx_deltas: delta = tx_deltas[tx_hash] - height, conf, timestamp = self.get_tx_height(tx_hash) - history.append((tx_hash, height, conf, timestamp, delta)) + tx_mined_status = self.get_tx_height(tx_hash) + history.append((tx_hash, tx_mined_status, delta)) history.sort(key = lambda x: self.get_txpos(x[0])) history.reverse() # 3. add balance c, u, x = self.get_balance(domain) balance = c + u + x h2 = [] - for tx_hash, height, conf, timestamp, delta in history: - h2.append((tx_hash, height, conf, timestamp, delta, balance)) + for tx_hash, tx_mined_status, delta in history: + h2.append((tx_hash, tx_mined_status, delta, balance)) if balance is None or delta is None: balance = None else: @@ -503,25 +508,27 @@ class AddressSynchronizer(PrintError): self._history_local[addr] = cur_hist def add_unverified_tx(self, tx_hash, tx_height): - if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \ - and tx_hash in self.verified_tx: + if tx_hash in self.verified_tx: + if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT): + with self.lock: + self.verified_tx.pop(tx_hash) + if self.verifier: + self.verifier.remove_spv_proof_for_tx(tx_hash) + else: with self.lock: - self.verified_tx.pop(tx_hash) + # tx will be verified only if height > 0 + self.unverified_tx[tx_hash] = tx_height + # to remove pending proof requests: if self.verifier: self.verifier.remove_spv_proof_for_tx(tx_hash) - # tx will be verified only if height > 0 - if tx_hash not in self.verified_tx: - with self.lock: - self.unverified_tx[tx_hash] = tx_height - - def add_verified_tx(self, tx_hash, info): + def add_verified_tx(self, tx_hash: str, info: VerifiedTxInfo): # Remove from the unverified map and add to the verified map with self.lock: self.unverified_tx.pop(tx_hash, None) - self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos) - height, conf, timestamp = self.get_tx_height(tx_hash) - self.network.trigger_callback('verified', tx_hash, height, conf, timestamp) + self.verified_tx[tx_hash] = info + tx_mined_status = self.get_tx_height(tx_hash) + self.network.trigger_callback('verified', tx_hash, tx_mined_status) def get_unverified_txs(self): '''Returns a map from tx hash to transaction height''' @@ -532,13 +539,22 @@ class AddressSynchronizer(PrintError): '''Used by the verifier when a reorg has happened''' txs = set() with self.lock: - for tx_hash, item in list(self.verified_tx.items()): - tx_height, timestamp, pos = item + for tx_hash, info in list(self.verified_tx.items()): + tx_height = info.height if tx_height >= height: header = blockchain.read_header(tx_height) - # fixme: use block hash, not timestamp - if not header or header.get('timestamp') != timestamp: + if not header or hash_header(header) != info.header_hash: self.verified_tx.pop(tx_hash, None) + # NOTE: we should add these txns to self.unverified_tx, + # but with what height? + # If on the new fork after the reorg, the txn is at the + # same height, we will not get a status update for the + # address. If the txn is not mined or at a diff height, + # we should get a status update. Unless we put tx into + # unverified_tx, it will turn into local. So we put it + # into unverified_tx with the old height, and if we get + # a status update, that will overwrite it. + self.unverified_tx[tx_hash] = tx_height txs.add(tx_hash) return txs @@ -546,19 +562,19 @@ class AddressSynchronizer(PrintError): """ return last known height if we are offline """ return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) - def get_tx_height(self, tx_hash): - """ Given a transaction, returns (height, conf, timestamp) """ + def get_tx_height(self, tx_hash: str) -> TxMinedStatus: + """ Given a transaction, returns (height, conf, timestamp, header_hash) """ with self.lock: if tx_hash in self.verified_tx: - height, timestamp, pos = self.verified_tx[tx_hash] - conf = max(self.get_local_height() - height + 1, 0) - return height, conf, timestamp + info = self.verified_tx[tx_hash] + conf = max(self.get_local_height() - info.height + 1, 0) + return TxMinedStatus(info.height, conf, info.timestamp, info.header_hash) elif tx_hash in self.unverified_tx: height = self.unverified_tx[tx_hash] - return height, 0, None + return TxMinedStatus(height, 0, None, None) else: # local transaction - return TX_HEIGHT_LOCAL, 0, None + return TxMinedStatus(TX_HEIGHT_LOCAL, 0, None, None) def set_up_to_date(self, up_to_date): with self.lock: diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py @@ -131,8 +131,8 @@ class HistoryScreen(CScreen): d = LabelDialog(_('Enter Transaction Label'), text, callback) d.open() - def get_card(self, tx_hash, height, conf, timestamp, value, balance): - status, status_str = self.app.wallet.get_tx_status(tx_hash, height, conf, timestamp) + def get_card(self, tx_hash, tx_mined_status, value, balance): + status, status_str = self.app.wallet.get_tx_status(tx_hash, tx_mined_status) icon = "atlas://electrum/gui/kivy/theming/light/" + TX_ICONS[status] label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs') ri = {} @@ -141,7 +141,7 @@ class HistoryScreen(CScreen): ri['icon'] = icon ri['date'] = status_str ri['message'] = label - ri['confirmations'] = conf + ri['confirmations'] = tx_mined_status.conf if value is not None: ri['is_mine'] = value < 0 if value < 0: value = - value @@ -158,7 +158,6 @@ class HistoryScreen(CScreen): return history = reversed(self.app.wallet.get_history()) history_card = self.screen.ids.history_container - count = 0 history_card.data = [self.get_card(*item) for item in history] diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py @@ -29,7 +29,7 @@ import datetime from electrum.address_synchronizer import TX_HEIGHT_LOCAL from .util import * from electrum.i18n import _ -from electrum.util import block_explorer_URL, profiler, print_error +from electrum.util import block_explorer_URL, profiler, print_error, TxMinedStatus try: from electrum.plot import plot_history, NothingToPlotException @@ -237,7 +237,8 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): value = tx_item['value'].value balance = tx_item['balance'].value label = tx_item['label'] - status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp) + tx_mined_status = TxMinedStatus(height, conf, timestamp, None) + status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) has_invoice = self.wallet.invoices.paid.get(tx_hash) icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) v_str = self.parent.format_amount(value, is_diff=True, whitespaces=True) @@ -304,10 +305,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): label = self.wallet.get_label(txid) item.setText(3, label) - def update_item(self, tx_hash, height, conf, timestamp): + def update_item(self, tx_hash, tx_mined_status): if self.wallet is None: return - status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp) + conf = tx_mined_status.conf + status, status_str = self.wallet.get_tx_status(tx_hash, tx_mined_status) icon = self.icon_cache.get(":icons/" + TX_ICONS[status]) items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1) if items: @@ -332,7 +334,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): column_title = self.headerItem().text(column) column_data = item.text(column) tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) - height, conf, timestamp = self.wallet.get_tx_height(tx_hash) + height = self.wallet.get_tx_height(tx_hash).height tx = self.wallet.transactions.get(tx_hash) is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) is_unconfirmed = height <= 0 diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py @@ -87,9 +87,9 @@ class ElectrumGui: + "%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s" messages = [] - for item in self.wallet.get_history(): - tx_hash, height, conf, timestamp, delta, balance = item - if conf: + for tx_hash, tx_mined_status, delta, balance in self.wallet.get_history(): + if tx_mined_status.conf: + timestamp = tx_mined_status.timestamp try: time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] except Exception: diff --git a/electrum/gui/text.py b/electrum/gui/text.py @@ -109,9 +109,9 @@ class ElectrumGui: b = 0 self.history = [] - for item in self.wallet.get_history(): - tx_hash, height, conf, timestamp, value, balance = item - if conf: + for tx_hash, tx_mined_status, value, balance in self.wallet.get_history(): + if tx_mined_status.conf: + timestamp = tx_mined_status.timestamp try: time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] except Exception: diff --git a/electrum/storage.py b/electrum/storage.py @@ -44,7 +44,7 @@ from .keystore import bip44_derivation OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 17 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 18 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format @@ -356,6 +356,7 @@ class WalletStorage(JsonDB): self.convert_version_15() self.convert_version_16() self.convert_version_17() + self.convert_version_18() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.write() @@ -570,6 +571,15 @@ class WalletStorage(JsonDB): self.put('seed_version', 17) + def convert_version_18(self): + # delete verified_tx3 as its structure changed + if not self._is_upgrade_method_needed(17, 17): + return + + self.put('verified_tx3', None) + + self.put('seed_version', 18) + def convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return diff --git a/electrum/util.py b/electrum/util.py @@ -23,6 +23,7 @@ import binascii import os, sys, re, json from collections import defaultdict +from typing import NamedTuple from datetime import datetime import decimal from decimal import Decimal @@ -903,3 +904,13 @@ def make_dir(path, allow_symlink=True): raise Exception('Dangling link: ' + path) os.mkdir(path) os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + + +TxMinedStatus = NamedTuple("TxMinedStatus", [("height", int), + ("conf", int), + ("timestamp", int), + ("header_hash", str)]) +VerifiedTxInfo = NamedTuple("VerifiedTxInfo", [("height", int), + ("timestamp", int), + ("txpos", int), + ("header_hash", str)]) diff --git a/electrum/verifier.py b/electrum/verifier.py @@ -23,9 +23,10 @@ from typing import Sequence, Optional -from .util import ThreadJob, bh2u +from .util import ThreadJob, bh2u, VerifiedTxInfo from .bitcoin import Hash, hash_decode, hash_encode from .transaction import Transaction +from .blockchain import hash_header class MerkleVerificationFailure(Exception): pass @@ -108,7 +109,9 @@ class SPV(ThreadJob): self.requested_merkle.remove(tx_hash) except KeyError: pass self.print_error("verified %s" % tx_hash) - self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos)) + header_hash = hash_header(header) + vtx_info = VerifiedTxInfo(tx_height, header.get('timestamp'), pos, header_hash) + self.wallet.add_verified_tx(tx_hash, vtx_info) if self.is_up_to_date() and self.wallet.is_up_to_date(): self.wallet.save_verified_tx(write=True) diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -318,7 +318,8 @@ class Abstract_Wallet(AddressSynchronizer): if tx.is_complete(): if tx_hash in self.transactions.keys(): label = self.get_label(tx_hash) - height, conf, timestamp = self.get_tx_height(tx_hash) + tx_mined_status = self.get_tx_height(tx_hash) + height, conf = tx_mined_status.height, tx_mined_status.conf if height > 0: if conf: status = _("{} confirmations").format(conf) @@ -368,8 +369,9 @@ class Abstract_Wallet(AddressSynchronizer): def balance_at_timestamp(self, domain, target_timestamp): h = self.get_history(domain) - for tx_hash, height, conf, timestamp, value, balance in h: - if timestamp > target_timestamp: + balance = 0 + for tx_hash, tx_mined_status, value, balance in h: + if tx_mined_status.timestamp > target_timestamp: return balance - value # return last balance return balance @@ -384,16 +386,17 @@ class Abstract_Wallet(AddressSynchronizer): fiat_income = Decimal(0) fiat_expenditures = Decimal(0) h = self.get_history(domain) - for tx_hash, height, conf, timestamp, value, balance in h: + for tx_hash, tx_mined_status, value, balance in h: + timestamp = tx_mined_status.timestamp if from_timestamp and (timestamp or time.time()) < from_timestamp: continue if to_timestamp and (timestamp or time.time()) >= to_timestamp: continue item = { - 'txid':tx_hash, - 'height':height, - 'confirmations':conf, - 'timestamp':timestamp, + 'txid': tx_hash, + 'height': tx_mined_status.height, + 'confirmations': tx_mined_status.conf, + 'timestamp': timestamp, 'value': Satoshis(value), 'balance': Satoshis(balance) } @@ -483,9 +486,12 @@ class Abstract_Wallet(AddressSynchronizer): return ', '.join(labels) return '' - def get_tx_status(self, tx_hash, height, conf, timestamp): + def get_tx_status(self, tx_hash, tx_mined_status): from .util import format_time extra = [] + height = tx_mined_status.height + conf = tx_mined_status.conf + timestamp = tx_mined_status.timestamp if conf == 0: tx = self.transactions.get(tx_hash) if not tx: @@ -839,8 +845,7 @@ class Abstract_Wallet(AddressSynchronizer): txid, n = txo.split(':') info = self.verified_tx.get(txid) if info: - tx_height, timestamp, pos = info - conf = local_height - tx_height + conf = local_height - info.height else: conf = 0 l.append((conf, v)) @@ -1091,7 +1096,7 @@ class Abstract_Wallet(AddressSynchronizer): def price_at_timestamp(self, txid, price_func): """Returns fiat price of bitcoin at the time tx got confirmed.""" - height, conf, timestamp = self.get_tx_height(txid) + timestamp = self.get_tx_height(txid).timestamp return price_func(timestamp if timestamp else time.time()) def unrealized_gains(self, domain, price_func, ccy): @@ -1258,7 +1263,7 @@ class Imported_Wallet(Simple_Wallet): self.verified_tx.pop(tx_hash, None) self.unverified_tx.pop(tx_hash, None) self.transactions.pop(tx_hash, None) - self.storage.put('verified_tx3', self.verified_tx) + self.save_verified_tx() self.save_transactions() self.set_label(address, None)