diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 587850405..6d194e0cf 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -31,6 +31,7 @@ from .util import PrintError, profiler, bfh 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,7 +63,7 @@ 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. + # Verified transactions. txid -> (height, timestamp, block_pos, block_hash). Access with self.lock. self.verified_tx = storage.get('verified_tx3', {}) # Transactions pending verification. txid -> tx_height. Access with self.lock. self.unverified_tx = defaultdict(int) @@ -434,7 +436,7 @@ class AddressSynchronizer(PrintError): "return position, even if the tx is unverified" with self.lock: if tx_hash in self.verified_tx: - height, timestamp, pos = self.verified_tx[tx_hash] + height, timestamp, pos, header_hash = self.verified_tx[tx_hash] return height, pos elif tx_hash in self.unverified_tx: height = self.unverified_tx[tx_hash] @@ -462,7 +464,7 @@ class AddressSynchronizer(PrintError): history = [] for tx_hash in tx_deltas: delta = tx_deltas[tx_hash] - height, conf, timestamp = self.get_tx_height(tx_hash) + height, conf, timestamp, header_hash = self.get_tx_height(tx_hash) history.append((tx_hash, height, conf, timestamp, delta)) history.sort(key = lambda x: self.get_txpos(x[0])) history.reverse() @@ -503,24 +505,26 @@ 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): # 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.verified_tx[tx_hash] = info # (tx_height, timestamp, pos, header_hash) + height, conf, timestamp, header_hash = self.get_tx_height(tx_hash) self.network.trigger_callback('verified', tx_hash, height, conf, timestamp) def get_unverified_txs(self): @@ -533,12 +537,21 @@ class AddressSynchronizer(PrintError): txs = set() with self.lock: for tx_hash, item in list(self.verified_tx.items()): - tx_height, timestamp, pos = item + tx_height, timestamp, pos, header_hash = item 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) != 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 @@ -547,18 +560,18 @@ class AddressSynchronizer(PrintError): 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) """ + """ 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] + height, timestamp, pos, header_hash = self.verified_tx[tx_hash] conf = max(self.get_local_height() - height + 1, 0) - return height, conf, timestamp + return height, conf, timestamp, header_hash elif tx_hash in self.unverified_tx: height = self.unverified_tx[tx_hash] - return height, 0, None + return height, 0, None, None else: # local transaction - return TX_HEIGHT_LOCAL, 0, None + return TX_HEIGHT_LOCAL, 0, None, None def set_up_to_date(self, up_to_date): with self.lock: diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 8f1a5ab31..602792a82 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -332,7 +332,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, conf, timestamp, header_hash = self.wallet.get_tx_height(tx_hash) 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/storage.py b/electrum/storage.py index 40c13289e..156df968e 100644 --- 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/verifier.py b/electrum/verifier.py index 82ad678bc..f53f8d1e0 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -26,6 +26,7 @@ from typing import Sequence, Optional from .util import ThreadJob, bh2u from .bitcoin import Hash, hash_decode, hash_encode from .transaction import Transaction +from .blockchain import hash_header class MerkleVerificationFailure(Exception): pass @@ -108,7 +109,8 @@ 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) + self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos, header_hash)) 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 index 3f45e9edf..430610f62 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -318,7 +318,7 @@ 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) + height, conf, timestamp, header_hash = self.get_tx_height(tx_hash) if height > 0: if conf: status = _("{} confirmations").format(conf) @@ -839,7 +839,7 @@ class Abstract_Wallet(AddressSynchronizer): txid, n = txo.split(':') info = self.verified_tx.get(txid) if info: - tx_height, timestamp, pos = info + tx_height, timestamp, pos, header_hash = info conf = local_height - tx_height else: conf = 0 @@ -1091,7 +1091,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) + height, conf, timestamp, header_hash = self.get_tx_height(txid) return price_func(timestamp if timestamp else time.time()) def unrealized_gains(self, domain, price_func, ccy):