Browse Source

verifier: better handle reorgs (and storage upgrade)

master
SomberNight 7 years ago
parent
commit
41e088693d
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 49
      electrum/address_synchronizer.py
  2. 2
      electrum/gui/qt/history_list.py
  3. 12
      electrum/storage.py
  4. 4
      electrum/verifier.py
  5. 6
      electrum/wallet.py

49
electrum/address_synchronizer.py

@ -31,6 +31,7 @@ from .util import PrintError, profiler, bfh
from .transaction import Transaction from .transaction import Transaction
from .synchronizer import Synchronizer from .synchronizer import Synchronizer
from .verifier import SPV from .verifier import SPV
from .blockchain import hash_header
from .i18n import _ from .i18n import _
TX_HEIGHT_LOCAL = -2 TX_HEIGHT_LOCAL = -2
@ -45,6 +46,7 @@ class UnrelatedTransactionException(AddTransactionException):
def __str__(self): def __str__(self):
return _("Transaction is unrelated to this wallet.") return _("Transaction is unrelated to this wallet.")
class AddressSynchronizer(PrintError): class AddressSynchronizer(PrintError):
""" """
inherited by wallet inherited by wallet
@ -61,7 +63,7 @@ class AddressSynchronizer(PrintError):
self.transaction_lock = threading.RLock() self.transaction_lock = threading.RLock()
# address -> list(txid, height) # address -> list(txid, height)
self.history = storage.get('addr_history',{}) 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', {}) self.verified_tx = storage.get('verified_tx3', {})
# Transactions pending verification. txid -> tx_height. Access with self.lock. # Transactions pending verification. txid -> tx_height. Access with self.lock.
self.unverified_tx = defaultdict(int) self.unverified_tx = defaultdict(int)
@ -434,7 +436,7 @@ class AddressSynchronizer(PrintError):
"return position, even if the tx is unverified" "return position, even if the tx is unverified"
with self.lock: with self.lock:
if tx_hash in self.verified_tx: 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 return height, pos
elif tx_hash in self.unverified_tx: elif tx_hash in self.unverified_tx:
height = self.unverified_tx[tx_hash] height = self.unverified_tx[tx_hash]
@ -462,7 +464,7 @@ class AddressSynchronizer(PrintError):
history = [] history = []
for tx_hash in tx_deltas: for tx_hash in tx_deltas:
delta = tx_deltas[tx_hash] 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.append((tx_hash, height, conf, timestamp, delta))
history.sort(key = lambda x: self.get_txpos(x[0])) history.sort(key = lambda x: self.get_txpos(x[0]))
history.reverse() history.reverse()
@ -503,24 +505,26 @@ class AddressSynchronizer(PrintError):
self._history_local[addr] = cur_hist self._history_local[addr] = cur_hist
def add_unverified_tx(self, tx_hash, tx_height): def add_unverified_tx(self, tx_hash, tx_height):
if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) \ if tx_hash in self.verified_tx:
and tx_hash in self.verified_tx: if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
with self.lock: with self.lock:
self.verified_tx.pop(tx_hash) self.verified_tx.pop(tx_hash)
if self.verifier: if self.verifier:
self.verifier.remove_spv_proof_for_tx(tx_hash) self.verifier.remove_spv_proof_for_tx(tx_hash)
else:
# tx will be verified only if height > 0
if tx_hash not in self.verified_tx:
with self.lock: with self.lock:
# tx will be verified only if height > 0
self.unverified_tx[tx_hash] = tx_height self.unverified_tx[tx_hash] = tx_height
# to remove pending proof requests:
if self.verifier:
self.verifier.remove_spv_proof_for_tx(tx_hash)
def add_verified_tx(self, tx_hash, info): def add_verified_tx(self, tx_hash, info):
# Remove from the unverified map and add to the verified map # Remove from the unverified map and add to the verified map
with self.lock: with self.lock:
self.unverified_tx.pop(tx_hash, None) self.unverified_tx.pop(tx_hash, None)
self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos) self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos, header_hash)
height, conf, timestamp = self.get_tx_height(tx_hash) height, conf, timestamp, header_hash = self.get_tx_height(tx_hash)
self.network.trigger_callback('verified', tx_hash, height, conf, timestamp) self.network.trigger_callback('verified', tx_hash, height, conf, timestamp)
def get_unverified_txs(self): def get_unverified_txs(self):
@ -533,12 +537,21 @@ class AddressSynchronizer(PrintError):
txs = set() txs = set()
with self.lock: with self.lock:
for tx_hash, item in list(self.verified_tx.items()): 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: if tx_height >= height:
header = blockchain.read_header(tx_height) header = blockchain.read_header(tx_height)
# fixme: use block hash, not timestamp if not header or hash_header(header) != header_hash:
if not header or header.get('timestamp') != timestamp:
self.verified_tx.pop(tx_hash, None) 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) txs.add(tx_hash)
return txs 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) return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
def get_tx_height(self, tx_hash): 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: with self.lock:
if tx_hash in self.verified_tx: 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) 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: elif tx_hash in self.unverified_tx:
height = self.unverified_tx[tx_hash] height = self.unverified_tx[tx_hash]
return height, 0, None return height, 0, None, None
else: else:
# local transaction # local transaction
return TX_HEIGHT_LOCAL, 0, None return TX_HEIGHT_LOCAL, 0, None, None
def set_up_to_date(self, up_to_date): def set_up_to_date(self, up_to_date):
with self.lock: with self.lock:

2
electrum/gui/qt/history_list.py

@ -332,7 +332,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
column_title = self.headerItem().text(column) column_title = self.headerItem().text(column)
column_data = item.text(column) column_data = item.text(column)
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) 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) tx = self.wallet.transactions.get(tx_hash)
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
is_unconfirmed = height <= 0 is_unconfirmed = height <= 0

12
electrum/storage.py

@ -44,7 +44,7 @@ from .keystore import bip44_derivation
OLD_SEED_VERSION = 4 # electrum versions < 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # 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 # old versions from overwriting new format
@ -356,6 +356,7 @@ class WalletStorage(JsonDB):
self.convert_version_15() self.convert_version_15()
self.convert_version_16() self.convert_version_16()
self.convert_version_17() self.convert_version_17()
self.convert_version_18()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self.write() self.write()
@ -570,6 +571,15 @@ class WalletStorage(JsonDB):
self.put('seed_version', 17) 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): def convert_imported(self):
if not self._is_upgrade_method_needed(0, 13): if not self._is_upgrade_method_needed(0, 13):
return return

4
electrum/verifier.py

@ -26,6 +26,7 @@ from typing import Sequence, Optional
from .util import ThreadJob, bh2u from .util import ThreadJob, bh2u
from .bitcoin import Hash, hash_decode, hash_encode from .bitcoin import Hash, hash_decode, hash_encode
from .transaction import Transaction from .transaction import Transaction
from .blockchain import hash_header
class MerkleVerificationFailure(Exception): pass class MerkleVerificationFailure(Exception): pass
@ -108,7 +109,8 @@ class SPV(ThreadJob):
self.requested_merkle.remove(tx_hash) self.requested_merkle.remove(tx_hash)
except KeyError: pass except KeyError: pass
self.print_error("verified %s" % tx_hash) 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(): if self.is_up_to_date() and self.wallet.is_up_to_date():
self.wallet.save_verified_tx(write=True) self.wallet.save_verified_tx(write=True)

6
electrum/wallet.py

@ -318,7 +318,7 @@ class Abstract_Wallet(AddressSynchronizer):
if tx.is_complete(): if tx.is_complete():
if tx_hash in self.transactions.keys(): if tx_hash in self.transactions.keys():
label = self.get_label(tx_hash) 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 height > 0:
if conf: if conf:
status = _("{} confirmations").format(conf) status = _("{} confirmations").format(conf)
@ -839,7 +839,7 @@ class Abstract_Wallet(AddressSynchronizer):
txid, n = txo.split(':') txid, n = txo.split(':')
info = self.verified_tx.get(txid) info = self.verified_tx.get(txid)
if info: if info:
tx_height, timestamp, pos = info tx_height, timestamp, pos, header_hash = info
conf = local_height - tx_height conf = local_height - tx_height
else: else:
conf = 0 conf = 0
@ -1091,7 +1091,7 @@ class Abstract_Wallet(AddressSynchronizer):
def price_at_timestamp(self, txid, price_func): def price_at_timestamp(self, txid, price_func):
"""Returns fiat price of bitcoin at the time tx got confirmed.""" """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()) return price_func(timestamp if timestamp else time.time())
def unrealized_gains(self, domain, price_func, ccy): def unrealized_gains(self, domain, price_func, ccy):

Loading…
Cancel
Save